Files
baiyue-firstleg/scripts/admin-test-env-modules/manual-state-loading-pda.mjs
T
2026-08-12 20:36:11 +08:00

287 lines
17 KiB
JavaScript

const MODULE = 'manual-state-loading-pda'
const OPTIONS = { scenario: 'manual-loading-pda-state-matrix', deploymentBlocker: true }
const CONTAINERS = '/api/platform/v1/containers'
const SMART = '/api/platform/v1/first-leg/smart-loading'
const LOADING = '/api/platform/v1/first-leg'
const PDA = `${LOADING}/pda-loading`
const CONTAINER_STATUSES = ['WAIT_LOADING', 'EDITING', 'LOADING', 'COMPLETED']
const PRELOAD_STATUSES = ['EDITING', 'LOADING']
const PDA_STATUSES = ['UNASSIGNED', 'IN_PROGRESS', 'AWAITING_REVIEW', 'COMPLETED']
function id(value) { return value == null ? '' : String(value) }
function records(value) { return Array.isArray(value?.records) ? value.records : [] }
function suffix(context, label) {
return `${String(context.marker).replace(/[^A-Za-z0-9]/g, '').slice(-10)}${label}`.slice(-20)
}
function operationId(context, label) {
return `${context.marker}-manual-loading-${label}-${crypto.randomUUID()}`.slice(0, 128)
}
async function post(context, path, body, label) {
return context.request('POST', path, body, context.token,
{ 'Idempotency-Key': operationId(context, label) })
}
async function prepareInbound(context, label) {
const originalMarker = context.marker
const fixtureMarker = `${originalMarker}-MAN-${label}`
const createdKeys = ['orders', 'ocrInbound', 'testInbound', 'inboundMeasurement', 'inboundWarehouseResources']
const contextKeys = ['inboundWeightOcr', 'ocrInbound', 'testInbound', 'createdInbound']
const createdSnapshot = new Map(createdKeys.map((key) => [key,
{ present: Object.hasOwn(context.createdResources, key), value: context.createdResources[key] }]))
const contextSnapshot = new Map(contextKeys.map((key) => [key,
{ present: Object.hasOwn(context.context, key), value: context.context[key] }]))
try {
context.marker = fixtureMarker
const orders = await import(new URL('./orders.mjs', import.meta.url))
await orders.run(context)
const measurement = await import(new URL('./inbound-measurement.mjs', import.meta.url))
await measurement.run(context)
const measured = context.createdResources.inboundMeasurement
const order = context.createdResources.orders
if (!measured?.inboundId || !measured?.tenantId) throw new Error(`${label} 未生成独立 INBOUNDED 入库单`)
return { marker: fixtureMarker, tenantId: id(measured.tenantId), inboundId: id(measured.inboundId),
orderId: id(order?.orderId) }
} finally {
context.marker = originalMarker
for (const [key, snapshot] of createdSnapshot) {
if (snapshot.present) context.createdResources[key] = snapshot.value
else delete context.createdResources[key]
}
for (const [key, snapshot] of contextSnapshot) {
if (snapshot.present) context.context[key] = snapshot.value
else delete context.context[key]
}
}
}
async function readInbound(context, fixture) {
const response = await context.request('GET',
`/api/platform/inbounds/${fixture.inboundId}?tenantId=${encodeURIComponent(fixture.tenantId)}`)
const value = context.apiSuccess(MODULE, '读取人工装柜独立入库单', response, '', OPTIONS)
const warehouseId = id(value?.summary?.warehouseId)
context.assert(MODULE, '人工装柜来源入库单可独立排柜', response,
value?.summary?.processStatus === 'INBOUNDED' && value?.summary?.measureReviewStatus === 'APPROVED'
&& Boolean(warehouseId),
`inboundId=${fixture.inboundId}, status=${value?.summary?.processStatus || 'missing'}`, OPTIONS)
return { ...fixture, detail: value, warehouseId }
}
async function createWaitingContainer(context, warehouseId, label) {
const value = suffix(context, label)
const response = await context.request('POST', CONTAINERS, {
containerNo: null, cabinetNo: `MAN-CAB-${value}`, warehouseId, warehouseName: '人工验收仓',
billOfLadingNo: null, vesselName: null, voyageNo: null, containerType: '40HQ',
shippingCompanyId: null, shippingCompanyName: null, departurePortCode: 'CNSZX',
departurePortName: '深圳', destinationPortCode: 'USLAX', destinationPortName: '洛杉矶',
destinationWarehouseCode: null, destinationWarehouseName: null, capacityCbm: 68,
maxWeightKg: 28000, terminalTruckingCompany: null, loadingAt: null,
estimatedSailingAt: null, actualSailingAt: null, estimatedPortArrivalAt: null,
actualPortArrivalAt: null, estimatedWarehouseArrivalAt: null, actualWarehouseArrivalAt: null,
})
const container = context.apiSuccess(MODULE, `创建 ${label} 货柜`, response, '', OPTIONS)
if (!container?.id) throw new Error(`${label} 货柜创建失败`)
return container
}
async function createConfirmedContainer(context, source, label) {
const value = suffix(context, label)
const summary = source.detail.summary
const draft = {
customerId: source.tenantId, customerName: summary.customerName || 'IT 测试客户',
channelCode: summary.channelCode, channelName: summary.channelName, warehouseId: source.warehouseId,
warehouseName: summary.warehouseName, shippingCompanyId: null, shippingCompanyName: `人工船司-${value}`,
bookingAgentId: null, bookingAgentName: `人工代理-${value}`, departurePortCode: 'CNSZX',
departurePortName: '深圳', destinationPortCode: 'USLAX', destinationPortName: '洛杉矶',
placeOfDelivery: 'LAX', serviceTerm: 'CY/CY', freightTerm: 'PREPAID', remark: `${context.marker}-${label}`,
parties: [{ partyType: 'SHIPPER', partyName: `发货方-${value}` },
{ partyType: 'CONSIGNEE', partyName: `收货方-${value}` },
{ partyType: 'NOTIFY_PARTY', partyName: `通知方-${value}` }],
containerRequirements: [{ containerType: '40HQ', containerKind: 'FCL', capacityCbm: 68,
maxWeightKg: 28000, quantity: 1 }],
}
const createdResponse = await post(context, '/api/platform/v1/ocean-bookings',
{ expectedVersion: 0, data: draft }, `${label}-booking-create`)
const created = context.apiSuccess(MODULE, `${label} 创建订舱`, createdResponse, '', OPTIONS)
const bookingId = id(created?.oceanBooking?.id)
const submittedResponse = await post(context, `/api/platform/v1/ocean-bookings/${bookingId}/submit`,
{ expectedVersion: Number(created?.oceanBooking?.version), reason: context.marker }, `${label}-booking-submit`)
const submitted = context.apiSuccess(MODULE, `${label} 提交订舱`, submittedResponse, '', OPTIONS)
const confirmedResponse = await post(context, `/api/platform/v1/ocean-bookings/${bookingId}/confirm`,
{ expectedVersion: Number(submitted?.oceanBooking?.version), reason: context.marker }, `${label}-booking-confirm`)
const confirmed = context.apiSuccess(MODULE, `${label} 确认订舱`, confirmedResponse, '', OPTIONS)
const containerId = id(confirmed?.containerAllocations?.[0]?.containerBookingId)
if (!containerId) throw new Error(`${label} 确认订舱未生成柜次`)
return containerId
}
async function createPlanTask(context, label) {
const source = await readInbound(context, await prepareInbound(context, label))
const containerId = await createConfirmedContainer(context, source, label)
const candidateResponse = await context.request('GET',
`${SMART}/candidates?bookingId=${containerId}&warehouseId=${source.warehouseId}`)
const candidates = context.apiSuccess(MODULE, `${label} 查询同渠道候选货物`, candidateResponse, '', OPTIONS)
const candidate = Array.isArray(candidates)
? candidates.find((item) => id(item.inboundId) === source.inboundId && Number(item.availablePackageCount) > 0)
: null
if (!candidate?.inboundDetailId) throw new Error(`${label} 独立入库单未进入同渠道排柜候选`)
const planResponse = await context.request('POST', `${SMART}/plans`,
{ bookingId: containerId, remark: `${context.marker}-${label}` })
let plan = context.apiSuccess(MODULE, `${label} 创建智能排柜计划`, planResponse, '', OPTIONS)
const assignResponse = await context.request('POST', `${SMART}/plans/${plan.id}/assignments`, {
inboundId: source.inboundId, inboundDetailId: id(candidate.inboundDetailId), packageCount: 1,
expectedVersion: Number(plan.version), operationId: operationId(context, `${label}-assign`),
})
plan = context.apiSuccess(MODULE, `${label} 分配独立候选货物`, assignResponse, '', OPTIONS)
const pushResponse = await post(context, `${SMART}/plans/${plan.id}/push-preload`,
{ expectedVersion: Number(plan.version) }, `${label}-push`)
plan = context.apiSuccess(MODULE, `${label} 推送预装`, pushResponse, '', OPTIONS)
const detailResponse = await context.request('GET', `${CONTAINERS}/${containerId}`)
const container = context.apiSuccess(MODULE, `${label} 读取柜次`, detailResponse, '', OPTIONS)
const containerNo = `MAN-${suffix(context, label)}`.slice(0, 64)
const transportResponse = await context.request('PATCH', `${CONTAINERS}/${containerId}/transport`, {
containerNo, voyageNo: `V-${suffix(context, label)}`.slice(0, 64), version: Number(container.version),
reason: `${context.marker} 人工状态样本`,
})
context.apiSuccess(MODULE, `${label} 写入真实集装箱号并开放 PDA`, transportResponse, '', OPTIONS)
const taskResponse = await context.request('GET',
`${PDA}/tasks?loadingPlanId=${plan.id}&warehouseId=${source.warehouseId}`)
const task = context.apiSuccess(MODULE, `${label} 读取 PDA 任务`, taskResponse, '', OPTIONS)
if (!task?.id || !task?.items?.[0]?.id) throw new Error(`${label} PDA 任务未生成`)
return { source, containerId, containerNo, plan, task }
}
async function claim(context, chain, label) {
const response = await context.request('POST', `${PDA}/tasks/${chain.task.id}/claim`, {
expectedAssignmentVersion: Number(chain.task?.assignment?.version || 0),
operationId: operationId(context, `${label}-claim`),
})
chain.task = context.apiSuccess(MODULE, `${label} 领取 PDA 任务`, response, '', OPTIONS)
return chain
}
async function pickAll(context, chain, label) {
const snapshotResponse = await context.request('GET',
`${PDA}/tasks/${chain.task.id}/inventory-snapshot?warehouseId=${chain.source.warehouseId}`)
const snapshot = context.apiSuccess(MODULE, `${label} 获取 PDA 库存快照`, snapshotResponse, '', OPTIONS)
const response = await post(context, `${PDA}/tasks/${chain.task.id}/batch`, {
warehouseId: chain.source.warehouseId, loadingPlanId: id(chain.task.loadingPlanId),
containerBookingId: chain.containerId, containerNo: chain.task.currentContainerNo || null,
expectedVersion: Number(chain.task.version), inventorySnapshotToken: snapshot.snapshotToken,
items: chain.task.items.map((item) => ({ itemId: id(item.id), action: 'PICK',
pickedPackageCount: Number(item.plannedPackageCount), finalPickedPackageCount: null,
releaseMode: 'MANUAL', locationIds: [], version: Number(item.version), reason: null })),
}, `${label}-batch`)
chain.task = context.apiSuccess(MODULE, `${label} 完成 PDA 拣货`, response, '', OPTIONS)
return chain
}
async function complete(context, chain, label) {
const response = await context.request('POST', `${PDA}/tasks/${chain.task.id}/complete`, {
expectedTaskVersion: Number(chain.task.version), operationId: operationId(context, `${label}-complete`),
remark: `${context.marker} 人工完成状态样本`,
})
chain.task = context.apiSuccess(MODULE, `${label} 人工确认装柜完成`, response, '', OPTIONS)
return chain
}
async function verifyContainer(context, status, fixture) {
const response = await context.request('GET', `${CONTAINERS}?status=${status}&page=1&size=100`)
const page = context.apiSuccess(MODULE, `${status} 货柜可按列表状态筛选`, response, '', OPTIONS)
context.assert(MODULE, `${status} 货柜样本保留`, response,
records(page).some((item) => id(item.id) === fixture.id && item.planStatus === status),
`id=${fixture.id}`, OPTIONS)
}
async function verifyPlan(context, status, fixture) {
const response = await context.request('GET',
`${LOADING}/loading-plans?status=${status}&warehouseId=${fixture.warehouseId}&page=1&size=100`)
const page = context.apiSuccess(MODULE, `${status} 预装清单可筛选`, response, '', OPTIONS)
context.assert(MODULE, `${status} 预装清单样本保留`, response,
records(page).some((item) => id(item.planId) === fixture.id && item.status === status),
`planId=${fixture.id}`, OPTIONS)
const detailResponse = await context.request('GET',
`${LOADING}/loading-plans/${fixture.id}?warehouseId=${fixture.warehouseId}`)
const detail = context.apiSuccess(MODULE, `${status} 预装清单详情可打开`, detailResponse, '', OPTIONS)
context.assert(MODULE, `${status} 预装清单详情身份正确`, detailResponse,
id(detail?.summary?.planId) === fixture.id && detail?.summary?.status === status,
`planId=${fixture.id}, actual=${detail?.summary?.status || 'missing'}`, OPTIONS)
}
async function verifyTask(context, status, fixture) {
const response = await context.request('GET',
`${PDA}/tasks/${fixture.id}?warehouseId=${fixture.warehouseId}`)
const task = context.apiSuccess(MODULE, `${status} PDA 任务可打开`, response, '', OPTIONS)
context.assert(MODULE, `${status} PDA 任务样本保留`, response,
id(task?.id) === fixture.id && task?.workflowStatus === status,
`taskId=${fixture.id}, actual=${task?.workflowStatus || 'missing'}`, OPTIONS)
}
function fixture(value, status, detailPath, extra = {}) {
return { id: id(value), status, detailPath, ...extra }
}
export async function run(context, matrix = context.createdResources.manualStateMatrix) {
matrix.pages.containerLoading = { requiredStatuses: CONTAINER_STATUSES, fixtures: {} }
matrix.pages.preloadPlans = { requiredStatuses: PRELOAD_STATUSES, fixtures: {} }
matrix.pages.loadingLists = { requiredStatuses: ['COMPLETED'], fixtures: {} }
matrix.pages.pdaTasks = { requiredStatuses: PDA_STATUSES, fixtures: {} }
const warehouseId = '193000000000200001'
const waiting = await createWaitingContainer(context, warehouseId, 'WAIT')
matrix.pages.containerLoading.fixtures.WAIT_LOADING = fixture(waiting.id, 'WAIT_LOADING',
`/container-loading?containerId=${id(waiting.id)}`)
const editing = await createPlanTask(context, 'EDIT')
const inProgress = await claim(context, await createPlanTask(context, 'PROGRESS'), 'PROGRESS')
const awaiting = await pickAll(context,
await claim(context, await createPlanTask(context, 'REVIEW'), 'REVIEW'), 'REVIEW')
const completed = await complete(context, await pickAll(context,
await claim(context, await createPlanTask(context, 'DONE'), 'DONE'), 'DONE'), 'DONE')
const chains = { EDITING: editing, LOADING: inProgress, COMPLETED: completed }
for (const [status, chain] of Object.entries(chains)) {
matrix.pages.containerLoading.fixtures[status] = fixture(chain.containerId, status,
`/container-loading/${chain.containerId}/smart-loading`, { planId: id(chain.plan.id) })
}
for (const [status, chain] of Object.entries({ EDITING: editing, LOADING: inProgress })) {
matrix.pages.preloadPlans.fixtures[status] = fixture(chain.plan.id, status,
`/loading-plans/${chain.plan.id}?warehouseId=${chain.source.warehouseId}`,
{ warehouseId: chain.source.warehouseId })
}
matrix.pages.loadingLists.fixtures.COMPLETED = fixture(completed.plan.id, 'COMPLETED',
`/loading-lists?planId=${completed.plan.id}&warehouseId=${completed.source.warehouseId}`,
{ warehouseId: completed.source.warehouseId })
const taskChains = { UNASSIGNED: editing, IN_PROGRESS: inProgress, AWAITING_REVIEW: awaiting, COMPLETED: completed }
for (const [status, chain] of Object.entries(taskChains)) {
matrix.pages.pdaTasks.fixtures[status] = fixture(chain.task.id, status,
`/pages/picking/picking?taskId=${chain.task.id}&warehouseId=${chain.source.warehouseId}`,
{ warehouseId: chain.source.warehouseId, loadingPlanId: id(chain.plan.id) })
}
for (const [status, value] of Object.entries(matrix.pages.containerLoading.fixtures))
await verifyContainer(context, status, value)
for (const [status, value] of Object.entries(matrix.pages.preloadPlans.fixtures))
await verifyPlan(context, status, value)
for (const [status, value] of Object.entries(matrix.pages.pdaTasks.fixtures))
await verifyTask(context, status, value)
const listFixture = matrix.pages.loadingLists.fixtures.COMPLETED
const listResponse = await context.request('GET',
`${LOADING}/loading-lists?warehouseId=${listFixture.warehouseId}&page=1&size=100`)
const list = context.apiSuccess(MODULE, 'COMPLETED 装柜清单可从列表打开', listResponse, '', OPTIONS)
context.assert(MODULE, 'COMPLETED 装柜清单样本保留', listResponse,
records(list).some((item) => id(item.planId) === listFixture.id && item.status === 'COMPLETED'),
`planId=${listFixture.id}`, OPTIONS)
const listDetailResponse = await context.request('GET',
`${LOADING}/loading-lists/${listFixture.id}?warehouseId=${listFixture.warehouseId}`)
const listDetail = context.apiSuccess(MODULE, 'COMPLETED 装柜清单详情可打开', listDetailResponse, '', OPTIONS)
context.assert(MODULE, 'COMPLETED 装柜清单详情身份正确', listDetailResponse,
id(listDetail?.summary?.planId) === listFixture.id && listDetail?.summary?.status === 'COMPLETED',
`planId=${listFixture.id}, actual=${listDetail?.summary?.status || 'missing'}`, OPTIONS)
}