优化
This commit is contained in:
@@ -11,6 +11,12 @@ public interface LoadingInboundMapper {
|
|||||||
+ "FROM fl_inbound_order WHERE tenant_id=#{tenantId} AND id=#{inboundId} AND deleted=0")
|
+ "FROM fl_inbound_order WHERE tenant_id=#{tenantId} AND id=#{inboundId} AND deleted=0")
|
||||||
Snapshot find(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId);
|
Snapshot find(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId);
|
||||||
|
|
||||||
|
@Update("UPDATE fl_inbound_order SET process_status='LOADING', updated_by=#{operatorId}, "
|
||||||
|
+ "version=version+1 WHERE tenant_id=#{tenantId} AND id=#{inboundId} AND deleted=0 "
|
||||||
|
+ "AND process_status='WAIT_SPLIT' AND version=#{expectedVersion}")
|
||||||
|
int beginLoading(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
|
||||||
|
@Param("operatorId") long operatorId, @Param("expectedVersion") int expectedVersion);
|
||||||
|
|
||||||
@Select("""
|
@Select("""
|
||||||
SELECT batch.source_inbound_id AS sourceInboundId,
|
SELECT batch.source_inbound_id AS sourceInboundId,
|
||||||
batch.loaded_child_inbound_id AS loadedChildInboundId,
|
batch.loaded_child_inbound_id AS loadedChildInboundId,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import com.baiyue.firstleg.entity.LoadingPickAssignmentEntity;
|
|||||||
import com.baiyue.firstleg.entity.LoadingPickOperationEntity;
|
import com.baiyue.firstleg.entity.LoadingPickOperationEntity;
|
||||||
import com.baiyue.firstleg.entity.LoadingPickReviewEntity;
|
import com.baiyue.firstleg.entity.LoadingPickReviewEntity;
|
||||||
import com.baiyue.firstleg.loading.mapper.InboundLoadingPickTaskMapper;
|
import com.baiyue.firstleg.loading.mapper.InboundLoadingPickTaskMapper;
|
||||||
|
import com.baiyue.firstleg.loading.mapper.LoadingInboundMapper;
|
||||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
@@ -40,6 +41,8 @@ public class LoadingPickRepository {
|
|||||||
private LoadingPickOperationMapper operations;
|
private LoadingPickOperationMapper operations;
|
||||||
@Resource
|
@Resource
|
||||||
private InboundLoadingPickTaskMapper inboundProjections;
|
private InboundLoadingPickTaskMapper inboundProjections;
|
||||||
|
@Resource
|
||||||
|
private LoadingInboundMapper inbounds;
|
||||||
|
|
||||||
public LoadingPickTaskEntity task(long taskId) {
|
public LoadingPickTaskEntity task(long taskId) {
|
||||||
return tasks.selectById(taskId);
|
return tasks.selectById(taskId);
|
||||||
@@ -209,6 +212,18 @@ public class LoadingPickRepository {
|
|||||||
return items.selectInboundProgress(tenantId, inboundId);
|
return items.selectInboundProgress(tenantId, inboundId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<LoadingPickItemMapper.InboundOwner> inboundOwners(long taskId) {
|
||||||
|
return items.selectInboundOwners(taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LoadingInboundMapper.Snapshot inbound(long tenantId, long inboundId) {
|
||||||
|
return inbounds.find(tenantId, inboundId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int beginInboundLoading(long tenantId, long inboundId, long operatorId, int expectedVersion) {
|
||||||
|
return inbounds.beginLoading(tenantId, inboundId, operatorId, expectedVersion);
|
||||||
|
}
|
||||||
|
|
||||||
public int settlePartial(long planItemId, long pickItemId, int pickedCount, int plannedCount,
|
public int settlePartial(long planItemId, long pickItemId, int pickedCount, int plannedCount,
|
||||||
long operatorId, String operatorName) {
|
long operatorId, String operatorName) {
|
||||||
if (items.settlePartialPlanItem(planItemId, pickedCount, plannedCount,
|
if (items.settlePartialPlanItem(planItemId, pickedCount, plannedCount,
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ import com.baiyue.firstleg.entity.LoadingPickReviewEntity;
|
|||||||
import com.baiyue.firstleg.entity.LoadingPickTaskEntity;
|
import com.baiyue.firstleg.entity.LoadingPickTaskEntity;
|
||||||
import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
|
import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
|
||||||
import com.baiyue.firstleg.entity.LoadingPlanHistoryEntity;
|
import com.baiyue.firstleg.entity.LoadingPlanHistoryEntity;
|
||||||
|
import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
|
||||||
|
import com.baiyue.firstleg.loading.mapper.LoadingInboundMapper;
|
||||||
|
import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
|
||||||
import com.baiyue.firstleg.mapper.LoadingPickItemMapper.CartonLabelRow;
|
import com.baiyue.firstleg.mapper.LoadingPickItemMapper.CartonLabelRow;
|
||||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||||
import com.baiyue.firstleg.repository.SmartLoadingRepository;
|
import com.baiyue.firstleg.repository.SmartLoadingRepository;
|
||||||
@@ -70,6 +73,8 @@ public class LoadingPickService {
|
|||||||
@Resource
|
@Resource
|
||||||
private LoadingListService loadingListService;
|
private LoadingListService loadingListService;
|
||||||
@Resource
|
@Resource
|
||||||
|
private InboundStatusHistoryMapper inboundHistories;
|
||||||
|
@Resource
|
||||||
private PdaPickSettlementService settlementService;
|
private PdaPickSettlementService settlementService;
|
||||||
@Resource
|
@Resource
|
||||||
private WarehousePalletLocationClient warehouse;
|
private WarehousePalletLocationClient warehouse;
|
||||||
@@ -451,9 +456,7 @@ public class LoadingPickService {
|
|||||||
|
|
||||||
/** 进入PDA拣货任务即将预装柜计划推进为拣货中;状态推进和历史写入保持幂等。 */
|
/** 进入PDA拣货任务即将预装柜计划推进为拣货中;状态推进和历史写入保持幂等。 */
|
||||||
private void startPickingIfNeeded(LoadingPickTaskEntity task, long operatorId, String operatorName) {
|
private void startPickingIfNeeded(LoadingPickTaskEntity task, long operatorId, String operatorName) {
|
||||||
if (smartLoadingRepository.startPicking(task.getLoadingPlanId(), operatorId, operatorName) != 1) {
|
if (smartLoadingRepository.startPicking(task.getLoadingPlanId(), operatorId, operatorName) == 1) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
LoadingPlanHistoryEntity history = new LoadingPlanHistoryEntity();
|
LoadingPlanHistoryEntity history = new LoadingPlanHistoryEntity();
|
||||||
history.setLoadingPlanId(task.getLoadingPlanId());
|
history.setLoadingPlanId(task.getLoadingPlanId());
|
||||||
history.setOperationId("PDA-START-" + task.getId());
|
history.setOperationId("PDA-START-" + task.getId());
|
||||||
@@ -467,6 +470,40 @@ public class LoadingPickService {
|
|||||||
history.setOperationReason("进入PDA拣货任务");
|
history.setOperationReason("进入PDA拣货任务");
|
||||||
smartLoadingRepository.insert(history);
|
smartLoadingRepository.insert(history);
|
||||||
}
|
}
|
||||||
|
for (var owner : repository.inboundOwners(task.getId())) {
|
||||||
|
if (owner.tenantId() == null || owner.inboundId() == null) {
|
||||||
|
throw conflict("PDA明细缺少入库单归属");
|
||||||
|
}
|
||||||
|
LoadingInboundMapper.Snapshot inbound = repository.inbound(owner.tenantId(), owner.inboundId());
|
||||||
|
if (inbound == null || inbound.processStatus() == null) {
|
||||||
|
throw conflict("PDA关联入库单不存在");
|
||||||
|
}
|
||||||
|
if ("LOADING".equals(inbound.processStatus())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!"WAIT_SPLIT".equals(inbound.processStatus()) || inbound.version() == null) {
|
||||||
|
throw conflict("入库单当前状态不能进入装柜阶段");
|
||||||
|
}
|
||||||
|
if (repository.beginInboundLoading(owner.tenantId(), owner.inboundId(), operatorId,
|
||||||
|
inbound.version()) != 1) {
|
||||||
|
LoadingInboundMapper.Snapshot refreshed = repository.inbound(owner.tenantId(), owner.inboundId());
|
||||||
|
if (refreshed == null || !"LOADING".equals(refreshed.processStatus())) {
|
||||||
|
throw conflict("入库单状态已变化,请刷新后重试");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
InboundStatusHistoryEntity history = new InboundStatusHistoryEntity();
|
||||||
|
history.setTenantId(owner.tenantId());
|
||||||
|
history.setInboundId(owner.inboundId());
|
||||||
|
history.setFromStatus("WAIT_SPLIT");
|
||||||
|
history.setToStatus("LOADING");
|
||||||
|
history.setAction("PDA_START_LOADING");
|
||||||
|
history.setReason("进入PDA拣货任务");
|
||||||
|
history.setOperatorId(operatorId);
|
||||||
|
history.setOperatorName(operatorName);
|
||||||
|
inboundHistories.insert(history);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private LoadingPickTaskEntity requiredTask(long taskId) {
|
private LoadingPickTaskEntity requiredTask(long taskId) {
|
||||||
LoadingPickTaskEntity task = repository.task(taskId);
|
LoadingPickTaskEntity task = repository.task(taskId);
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ paths:
|
|||||||
/api/platform/v1/inbounds/{inboundId}/loading/evidence/pda:
|
/api/platform/v1/inbounds/{inboundId}/loading/evidence/pda:
|
||||||
get:
|
get:
|
||||||
summary: List loading evidence for the active PDA worker
|
summary: List loading evidence for the active PDA worker
|
||||||
|
description: Requires the associated inbound to have entered the loading stage. PDA workflowStatus AWAITING_REVIEW does not bypass this gate; canonical claim or picking advances WAIT_SPLIT to LOADING.
|
||||||
parameters: [{$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/TenantId'}]
|
parameters: [{$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/TenantId'}]
|
||||||
responses:
|
responses:
|
||||||
'200': {description: Business envelope; requires platform:firstleg:pda:work}
|
'200': {description: Business envelope; requires platform:firstleg:pda:work}
|
||||||
post:
|
post:
|
||||||
summary: Append one PDA-captured loading evidence photo
|
summary: Append one PDA-captured loading evidence photo
|
||||||
|
description: Requires the associated inbound process status to be LOADING. PDA workflowStatus AWAITING_REVIEW is a review projection and does not replace the inbound loading-stage gate.
|
||||||
parameters: [{$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
|
parameters: [{$ref: '#/components/parameters/InboundId'}, {$ref: '#/components/parameters/TenantId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ paths:
|
|||||||
/api/platform/v1/first-leg/pda-loading/tasks/{taskId}/claim:
|
/api/platform/v1/first-leg/pda-loading/tasks/{taskId}/claim:
|
||||||
post:
|
post:
|
||||||
summary: Atomically claim an unassigned task for the authenticated operator
|
summary: Atomically claim an unassigned task for the authenticated operator
|
||||||
|
description: Claiming the canonical PDA task idempotently starts its plan and advances every associated inbound from WAIT_SPLIT to LOADING with optimistic locking; workflowStatus may later become AWAITING_REVIEW while the task remains LOADING until manual completion.
|
||||||
parameters: [{$ref: '#/components/parameters/TaskId'}]
|
parameters: [{$ref: '#/components/parameters/TaskId'}]
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
@@ -66,6 +67,7 @@ paths:
|
|||||||
/api/platform/v1/first-leg/pda-loading/tasks/{taskId}/items/{itemId}/pick:
|
/api/platform/v1/first-leg/pda-loading/tasks/{taskId}/items/{itemId}/pick:
|
||||||
post:
|
post:
|
||||||
summary: Increment a picked item owned by the authenticated assignee
|
summary: Increment a picked item owned by the authenticated assignee
|
||||||
|
description: The first successful pick also repairs the same WAIT_SPLIT to LOADING transition when the plan was already started, without using containerNo as an association key.
|
||||||
parameters: [{$ref: '#/components/parameters/TaskId'}, {$ref: '#/components/parameters/ItemId'}]
|
parameters: [{$ref: '#/components/parameters/TaskId'}, {$ref: '#/components/parameters/ItemId'}]
|
||||||
responses: {'200': {description: Updated task}}
|
responses: {'200': {description: Updated task}}
|
||||||
/api/platform/v1/first-leg/pda-loading/tasks/{taskId}/items/{itemId}/partial:
|
/api/platform/v1/first-leg/pda-loading/tasks/{taskId}/items/{itemId}/partial:
|
||||||
@@ -81,6 +83,7 @@ paths:
|
|||||||
/api/platform/v1/first-leg/pda-loading/tasks/{taskId}/batch:
|
/api/platform/v1/first-leg/pda-loading/tasks/{taskId}/batch:
|
||||||
post:
|
post:
|
||||||
summary: Atomically submit the complete online PDA draft once
|
summary: Atomically submit the complete online PDA draft once
|
||||||
|
description: A batch containing PICK or PARTIAL idempotently advances associated inbound process status to LOADING before evidence operations become available.
|
||||||
parameters:
|
parameters:
|
||||||
- {$ref: '#/components/parameters/TaskId'}
|
- {$ref: '#/components/parameters/TaskId'}
|
||||||
- {name: Idempotency-Key, in: header, required: true, schema: {type: string, maxLength: 128}}
|
- {name: Idempotency-Key, in: header, required: true, schema: {type: string, maxLength: 128}}
|
||||||
|
|||||||
+3
@@ -30,6 +30,9 @@ class PdaConvergenceOpenApiContractTest {
|
|||||||
.contains("InventorySnapshot")
|
.contains("InventorySnapshot")
|
||||||
.contains("changing it never resynchronizes the task or item versions")
|
.contains("changing it never resynchronizes the task or item versions")
|
||||||
.contains("expectedTaskVersion")
|
.contains("expectedTaskVersion")
|
||||||
|
.contains("advances every associated inbound from WAIT_SPLIT to LOADING with optimistic locking")
|
||||||
|
.contains("workflowStatus may later become AWAITING_REVIEW")
|
||||||
|
.contains("without using containerNo as an association key")
|
||||||
.contains("CompleteLoadingRequest");
|
.contains("CompleteLoadingRequest");
|
||||||
|
|
||||||
String taskLookup = yaml.substring(
|
String taskLookup = yaml.substring(
|
||||||
|
|||||||
+2
@@ -25,6 +25,8 @@ class LoadingDeliveryOpenApiContractTest {
|
|||||||
.contains("同类型可多次追加")
|
.contains("同类型可多次追加")
|
||||||
.contains("Required when evidenceType is SEAL")
|
.contains("Required when evidenceType is SEAL")
|
||||||
.contains("compatibility projection")
|
.contains("compatibility projection")
|
||||||
|
.contains("associated inbound process status to be LOADING")
|
||||||
|
.contains("workflowStatus AWAITING_REVIEW does not bypass this gate")
|
||||||
.contains("must exactly match the server-derived container-task aggregate")
|
.contains("must exactly match the server-derived container-task aggregate")
|
||||||
.contains("cannot complete canonical picking");
|
.contains("cannot complete canonical picking");
|
||||||
assertThat(yaml).doesNotContain("/delivery/complete").doesNotContain("DELIVERED");
|
assertThat(yaml).doesNotContain("/delivery/complete").doesNotContain("DELIVERED");
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ import com.baiyue.firstleg.entity.LoadingPickItemEntity;
|
|||||||
import com.baiyue.firstleg.entity.LoadingPickOperationEntity;
|
import com.baiyue.firstleg.entity.LoadingPickOperationEntity;
|
||||||
import com.baiyue.firstleg.entity.LoadingPickTaskEntity;
|
import com.baiyue.firstleg.entity.LoadingPickTaskEntity;
|
||||||
import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
|
import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
|
||||||
|
import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
|
||||||
|
import com.baiyue.firstleg.loading.mapper.LoadingInboundMapper;
|
||||||
|
import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
|
||||||
|
import com.baiyue.firstleg.mapper.LoadingPickItemMapper;
|
||||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||||
import com.baiyue.firstleg.repository.SmartLoadingRepository;
|
import com.baiyue.firstleg.repository.SmartLoadingRepository;
|
||||||
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
|
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
|
||||||
@@ -53,6 +57,8 @@ class LoadingPickServiceTest {
|
|||||||
@MockBean
|
@MockBean
|
||||||
private LoadingListService loadingListService;
|
private LoadingListService loadingListService;
|
||||||
@MockBean
|
@MockBean
|
||||||
|
private InboundStatusHistoryMapper inboundHistories;
|
||||||
|
@MockBean
|
||||||
private PdaPickSettlementService settlementService;
|
private PdaPickSettlementService settlementService;
|
||||||
@MockBean
|
@MockBean
|
||||||
private WarehousePalletLocationClient warehouse;
|
private WarehousePalletLocationClient warehouse;
|
||||||
@@ -178,6 +184,51 @@ class LoadingPickServiceTest {
|
|||||||
verify(smartLoadingRepository).insert(any(com.baiyue.firstleg.entity.LoadingPlanHistoryEntity.class));
|
verify(smartLoadingRepository).insert(any(com.baiyue.firstleg.entity.LoadingPlanHistoryEntity.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void startsInboundLoadingEvenWhenPlanWasAlreadyStarted() {
|
||||||
|
LoadingPickTaskEntity task = task();
|
||||||
|
LoadingPickItemEntity item = item();
|
||||||
|
preparePick(task, item, "pick-start-inbound");
|
||||||
|
when(smartLoadingRepository.startPicking(10L, 9L, "operator")).thenReturn(0);
|
||||||
|
when(repository.inboundOwners(20L)).thenReturn(List.of(
|
||||||
|
new LoadingPickItemMapper.InboundOwner(101L, 50L)));
|
||||||
|
when(repository.inbound(101L, 50L)).thenReturn(
|
||||||
|
new LoadingInboundMapper.Snapshot(50L, 101L, "WAIT_SPLIT", 4));
|
||||||
|
when(repository.beginInboundLoading(101L, 50L, 9L, 4)).thenReturn(1);
|
||||||
|
|
||||||
|
service.pick(20L, 30L, new PickRequest(101L, 2, List.of(), 0, "pick-start-inbound"),
|
||||||
|
9L, "operator");
|
||||||
|
|
||||||
|
verify(repository).beginInboundLoading(101L, 50L, 9L, 4);
|
||||||
|
verify(inboundHistories).insert(argThat((InboundStatusHistoryEntity history) ->
|
||||||
|
Long.valueOf(101L).equals(history.getTenantId())
|
||||||
|
&& Long.valueOf(50L).equals(history.getInboundId())
|
||||||
|
&& "WAIT_SPLIT".equals(history.getFromStatus())
|
||||||
|
&& "LOADING".equals(history.getToStatus())
|
||||||
|
&& "PDA_START_LOADING".equals(history.getAction())));
|
||||||
|
verify(smartLoadingRepository, never()).insert(any(
|
||||||
|
com.baiyue.firstleg.entity.LoadingPlanHistoryEntity.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void acceptsConcurrentInboundTransitionWithoutDuplicateHistory() {
|
||||||
|
LoadingPickTaskEntity task = task();
|
||||||
|
LoadingPickItemEntity item = item();
|
||||||
|
preparePick(task, item, "pick-concurrent-inbound");
|
||||||
|
when(repository.inboundOwners(20L)).thenReturn(List.of(
|
||||||
|
new LoadingPickItemMapper.InboundOwner(101L, 50L)));
|
||||||
|
when(repository.inbound(101L, 50L)).thenReturn(
|
||||||
|
new LoadingInboundMapper.Snapshot(50L, 101L, "WAIT_SPLIT", 4),
|
||||||
|
new LoadingInboundMapper.Snapshot(50L, 101L, "LOADING", 5));
|
||||||
|
when(repository.beginInboundLoading(101L, 50L, 9L, 4)).thenReturn(0);
|
||||||
|
|
||||||
|
service.pick(20L, 30L, new PickRequest(101L, 2, List.of(), 0, "pick-concurrent-inbound"),
|
||||||
|
9L, "operator");
|
||||||
|
|
||||||
|
verify(repository).beginInboundLoading(101L, 50L, 9L, 4);
|
||||||
|
verify(inboundHistories, never()).insert(any(InboundStatusHistoryEntity.class));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void claimStartsLoadingAndSameOperatorReentryDoesNotDuplicateStartHistory() {
|
void claimStartsLoadingAndSameOperatorReentryDoesNotDuplicateStartHistory() {
|
||||||
LoadingPickTaskEntity task = task();
|
LoadingPickTaskEntity task = task();
|
||||||
|
|||||||
@@ -18,5 +18,5 @@ test('evidence gate requires stable PDA identity and server LOADING before any e
|
|||||||
assert.match(source, /AWAITING_REVIEW 不作为证据开放条件/)
|
assert.match(source, /AWAITING_REVIEW 不作为证据开放条件/)
|
||||||
const gate = source.slice(source.indexOf('const taskResponse'), source.indexOf('const path ='))
|
const gate = source.slice(source.indexOf('const taskResponse'), source.indexOf('const path ='))
|
||||||
assert.doesNotMatch(gate, /loading\/evidence/)
|
assert.doesNotMatch(gate, /loading\/evidence/)
|
||||||
assert.match(source, /const path = `\/api\/platform\/inbounds\/\$\{inboundId\}\/loading\/evidence/)
|
assert.match(source, /const path = `\/api\/platform\/v1\/inbounds\/\$\{inboundId\}\/loading\/evidence/)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,9 +3,14 @@ import { readFileSync } from 'node:fs'
|
|||||||
import test from 'node:test'
|
import test from 'node:test'
|
||||||
const source=readFileSync(new URL('./suppliers.mjs',import.meta.url),'utf8')
|
const source=readFileSync(new URL('./suppliers.mjs',import.meta.url),'utf8')
|
||||||
const orchestrator=readFileSync(new URL('../admin-test-env-acceptance.mjs',import.meta.url),'utf8')
|
const orchestrator=readFileSync(new URL('../admin-test-env-acceptance.mjs',import.meta.url),'utf8')
|
||||||
|
const openapi=readFileSync(new URL('../../warehouse/src/main/resources/openapi/supplier-v1.yaml',import.meta.url),'utf8')
|
||||||
|
|
||||||
test('supplier acceptance covers full CRUD-like lifecycle and boundary failures',()=>{
|
test('supplier acceptance covers full CRUD-like lifecycle and boundary failures',()=>{
|
||||||
for(const marker of ['列表分页与订舱筛选','查询四类供应商统计','新增供应商','查询供应商详情','编辑供应商','停用供应商','重复名称返回冲突','非订舱分类携带scope被拒绝','tenantId非0被拒绝','客户账号调用平台供应商接口被拒绝','requestId','traceId']) assert.match(source,new RegExp(marker))
|
for(const marker of ['列表分页与订舱筛选','查询四类供应商统计','新增供应商','查询供应商详情','编辑供应商','停用供应商','重复名称返回冲突','非订舱分类携带scope被拒绝','tenantId非0被拒绝','客户账号调用平台供应商接口被拒绝','requestId','traceId']) assert.match(source,new RegExp(marker))
|
||||||
})
|
})
|
||||||
test('supplier module is registered in the default full acceptance orchestration',()=>assert.match(orchestrator,/DEFAULT_MODULES = \[[^\]]*'suppliers'/s))
|
test('supplier module is registered in the default full acceptance orchestration',()=>assert.match(orchestrator,/CONFIGURED_MODULES = \[[^\]]*'suppliers'/s))
|
||||||
test('supplier fixture is retained only as inactive because API has no delete contract',()=>{assert.match(source,/finalStatus:inactive\?'INACTIVE'/);assert.doesNotMatch(source,/context\.request\('DELETE'/)})
|
test('supplier fixture is retained only as inactive because API has no delete contract',()=>{assert.match(source,/finalStatus:inactive\?'INACTIVE'/);assert.doesNotMatch(source,/context\.request\('DELETE'/)})
|
||||||
|
test('supplier OpenAPI exposes the internal active category and booking scope validation contract',()=>{
|
||||||
|
for(const marker of ['/internal/warehouse/v1/suppliers/{supplierId}','X-Internal-Service-Token','requireActive','InternalSnapshot','SupplierPageResponse','platform:supplier:list']) assert.match(openapi,new RegExp(marker.replace(/[{}]/g,'\\$&')))
|
||||||
|
assert.doesNotMatch(openapi,/platform:supplier:view/)
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user