fix(dropship): generate pallet numbers atomically and extend full regressions
This commit is contained in:
@@ -37,6 +37,10 @@ export async function run(context) {
|
||||
|
||||
## 可用上下文
|
||||
|
||||
货柜查询使用 `/api/platform/v1/containers/domestic` 列表和 `/domestic/{id}` 详情,响应为 `WorkbenchItem`(`container`、`workbenchStage`、`statusSummary`、`allowedActions`),不接受旧扁平实体。默认 `phase3-container-loading` 同时验证页面的 ACTIVE、状态、关键字组合查询及详情结构;`container-detail-route-contract.test.mjs` 离线核对 Admin、Controller 和人工分段/同柜脚本读路径,并反证旧 DTO。定向真实 HTTP 使用总入口 `--modules phase3-container-loading`,仍自动展开本轮前置并创建测试资源;不借历史柜。
|
||||
|
||||
一件代发的 `inbound-measurement` 前置通过 PDA 正式 `pallets/create-and-bind` 创建系统 DP 码并绑定待清点箱,不再按测试 marker 拼托盘号;默认一件代发后续链复用该前置。验证清点前及绑定后无待绑箱请求均拒绝且无空托、系统发码同操作重放保持托盘/计数不变、封托完成后原操作可重放;离线场景反向注入重复发码和空托,保证回归确实识别这两类故障。
|
||||
|
||||
- `request(method, path, body?, token?, extraHeaders?)`:JSON HTTP 请求,自动使用平台 Token;幂等写操作通过 `extraHeaders` 传 `Idempotency-Key`。
|
||||
- `requestBinary(method, path)`:文件下载,不把二进制写入报告。
|
||||
- `apiSuccess(module, name, response, evidence?, options?)`:断言 HTTP 200 + body code `200`,成功时返回 data。
|
||||
|
||||
@@ -224,9 +224,9 @@ async function createReleasedBookingAndLink(context, fixture, chains) {
|
||||
if (!requirement?.id) return null
|
||||
for (let index = 0; index < chains.length; index += 1) {
|
||||
const chain = chains[index]
|
||||
const detailResponse = await context.request('GET', `${CONTAINERS}/${chain.containerBookingId}`)
|
||||
const detailResponse = await context.request('GET', `${CONTAINERS}/domestic/${chain.containerBookingId}`)
|
||||
const detail = context.apiSuccess(MODULE, `${chain.label} 关联前读取柜版本`, detailResponse,
|
||||
evidence(detailResponse), OPTIONS)
|
||||
evidence(detailResponse), OPTIONS)?.container
|
||||
const containerNo = iso6346ContainerNumber(`${context.marker}-coload-${index + 1}`)
|
||||
const linkResponse = await context.request('POST', `${CONTAINERS}/${chain.containerBookingId}/ocean-booking-link`, {
|
||||
oceanBookingId, requirementId: id(requirement.id), containerNo,
|
||||
|
||||
@@ -1,9 +1,52 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
import { containerWorkbenchGaps } from './phase3-container-loading.mjs'
|
||||
|
||||
const read = (name) => readFileSync(new URL(name, import.meta.url), 'utf8')
|
||||
|
||||
test('workbench DTO rejects the old flat entity and incomplete action/state projections', () => {
|
||||
const container = { id: '71', cabinetNo: 'CB-20260909-01', version: 0 }
|
||||
const row = { container, workbenchStage: 'WAIT_LOADING', statusSummary: {}, allowedActions: [] }
|
||||
assert.deepEqual(containerWorkbenchGaps(row), [])
|
||||
assert.ok(containerWorkbenchGaps(container).includes('container.id'))
|
||||
for (const field of ['container', 'workbenchStage', 'statusSummary', 'allowedActions']) {
|
||||
const invalid = { ...row }
|
||||
delete invalid[field]
|
||||
assert.ok(containerWorkbenchGaps(invalid).length > 0, `missing ${field} must fail`)
|
||||
}
|
||||
assert.ok(containerWorkbenchGaps({ ...row, container: { ...container, version: null } }).includes('container.version'))
|
||||
})
|
||||
|
||||
test('Admin read routes match controller workbench endpoints and default HTTP regression exercises the page DTO', () => {
|
||||
const controller = read('../../firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformContainerController.java')
|
||||
const admin = read('../../../apps/admin-web/src/modules/container-loading/api/platformContainerLoading.ts')
|
||||
assert.match(controller, /@GetMapping\("\/domestic"\)/)
|
||||
assert.match(controller, /@GetMapping\("\/domestic\/\{id\}"\)/)
|
||||
assert.match(controller, /ApiResponse<PageResult<WorkbenchItem>>/)
|
||||
assert.match(admin, /listContainers\([^\n]*workbench: ContainerWorkbenchKind = 'DOMESTIC'/)
|
||||
assert.ok(admin.includes('`${containerPath}/${workbench.toLowerCase()}`'))
|
||||
assert.ok(admin.includes('`${containerPath}/${workbench.toLowerCase()}/${encodeURIComponent(containerId)}`'))
|
||||
const loading = read('./phase3-container-loading.mjs')
|
||||
assert.ok(loading.includes('size=20&scope=ACTIVE&status='))
|
||||
assert.ok(loading.includes('containerWorkbenchGaps(containerDetailResponse.body?.data)'))
|
||||
assert.ok(loading.includes('records(page).every((item) => containerWorkbenchGaps(item).length === 0)'))
|
||||
assert.ok(!loading.includes('value?.container || value'))
|
||||
})
|
||||
|
||||
test('manual and coload consumers read domestic WorkbenchItem rather than the removed flat routes', () => {
|
||||
for (const name of ['manual-loading-complete-fixture', 'manual-order-splitting-verify',
|
||||
'manual-pda-ready-fixture', 'manual-pda-ready-verify', 'manual-preload-push-fixture', 'manual-preload-push-verify', 'coload-groups']) {
|
||||
const source = read(`./${name}.mjs`)
|
||||
assert.ok(source.includes("context.request('GET', `${CONTAINERS}/domestic/${"), name)
|
||||
assert.ok(!source.includes("context.request('GET', `${CONTAINERS}/${"), name)
|
||||
assert.match(source, /OPTIONS\)\?\.container/, name)
|
||||
}
|
||||
const states = read('./manual-state-loading-pda.mjs')
|
||||
assert.ok(states.includes('`${CONTAINERS}/domestic?scope=ACTIVE&status='))
|
||||
assert.ok(states.includes('item.container?.planStatus === status'))
|
||||
})
|
||||
|
||||
test('domestic container fixtures use the current workbench detail route', () => {
|
||||
const containerLoading = read('./phase3-container-loading.mjs')
|
||||
assert.ok(containerLoading.includes("context.request('GET', `${DOMESTIC_CONTAINERS}/${containerId}`)"))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
import { completeDropshipPalletization } from './inbound-measurement.mjs'
|
||||
|
||||
const source = readFileSync(new URL('inbound-measurement.mjs', import.meta.url), 'utf8')
|
||||
const read = (relativeUrl) => readFileSync(new URL(relativeUrl, import.meta.url), 'utf8')
|
||||
@@ -102,7 +103,10 @@ test('dropship measurement completes the formal palletization workflow before sm
|
||||
assert.match(source, /pda-dropship\/tasks\/\$\{taskId\}\/claim/)
|
||||
assert.match(source, /cartons\/resolve/)
|
||||
assert.match(source, /cartons\/count/)
|
||||
assert.match(source, /pallets\/bind-pending/)
|
||||
assert.match(source, /pallets\/create-and-bind/)
|
||||
assert.match(source, /系统发码重放不新增托盘且业务码和计数稳定/)
|
||||
assert.match(source, /拒绝创建没有空托或计数漂移/)
|
||||
assert.doesNotMatch(source, /palletHash|palletDate/)
|
||||
assert.match(source, /pallets\/\$\{palletId\}\/seal/)
|
||||
assert.match(source, /重复清点操作按operationId幂等返回/)
|
||||
assert.doesNotMatch(source, /pallets\/open/)
|
||||
@@ -117,3 +121,73 @@ test('confirmed package count reconciles an uncertain transport result before re
|
||||
assert.match(source, /Number\(detail\?\.version\) > Number\(body\.measurementVersion\)/)
|
||||
assert.match(source, /await updateConfirmedPackageCount\(context, measurementPath/)
|
||||
})
|
||||
|
||||
async function exerciseSystemPalletScenario(fault = '') {
|
||||
const task = { taskId: '81', inboundId: '82', warehouseId: '83', taskStatus: 'CLAIMED',
|
||||
totalCartons: 2, countedCartons: 0, boundCartons: 0, unboundCartons: 0, pallets: [] }
|
||||
const seenCounts = new Map()
|
||||
const seenCreates = new Set()
|
||||
const checks = []
|
||||
const response = (data, code = '200') => ({ http: 200, body: { code, data: structuredClone(data) } })
|
||||
const context = {
|
||||
marker: 'fixture-system-pallet',
|
||||
apiSuccess(_module, name, result) {
|
||||
assert.equal(result.body.code, '200', name)
|
||||
return result.body.data
|
||||
},
|
||||
assert(_module, name, _response, condition) { checks.push({ name, passed: Boolean(condition) }) },
|
||||
async request(method, path, body) {
|
||||
if (path.includes('/pending?')) return response([task])
|
||||
if (path.endsWith('/claim') || (method === 'GET' && path.includes('/tasks/81?'))) return response(task)
|
||||
if (path.includes('/cartons/resolve?')) return response({ boxCode: new URL(path, 'http://fixture').searchParams.get('boxCode'), canCount: true, expectedSkuQuantity: 10 })
|
||||
if (path.endsWith('/cartons/count')) {
|
||||
if (seenCounts.has(body.operationId)) return seenCounts.get(body.operationId)
|
||||
task.countedCartons += 1
|
||||
const targetPallet = task.pallets.find(pallet => pallet.palletNo === body.palletNo)
|
||||
if (body.palletNo) { task.boundCartons += 1; targetPallet.cartonCount += 1; targetPallet.version += 1 }
|
||||
else task.unboundCartons += 1
|
||||
const result = response({ ...task, carton: { boxCode: body.boxCode, expectedSkuQuantity: 10, actualSkuQuantity: 10,
|
||||
discrepancyStatus: 'MATCH' }, pallet: body.palletNo ? targetPallet : null })
|
||||
seenCounts.set(body.operationId, result)
|
||||
return result
|
||||
}
|
||||
if (path.endsWith('/create-and-bind')) {
|
||||
assert.deepEqual(Object.keys(body).sort(), ['operationId', 'warehouseId'])
|
||||
if (seenCreates.has(body.operationId)) {
|
||||
if (fault === 'duplicate-replay') task.pallets.push({ ...task.pallets[0], palletId: '999', palletNo: 'DP-260909-0999' })
|
||||
return response(task)
|
||||
}
|
||||
if (!task.unboundCartons) {
|
||||
if (fault === 'empty-pallet') task.pallets.push({ palletId: '998', palletNo: 'DP-260909-0998', cartonCount: 0, status: 'OPEN', version: 0 })
|
||||
return response(null, 'A0409')
|
||||
}
|
||||
seenCreates.add(body.operationId)
|
||||
task.pallets.push({ palletId: '84', palletNo: 'DP-260909-0001', cartonCount: task.unboundCartons, status: 'OPEN', version: 0 })
|
||||
task.boundCartons += task.unboundCartons
|
||||
task.unboundCartons = 0
|
||||
return response(task)
|
||||
}
|
||||
if (path.endsWith('/84/seal')) {
|
||||
task.taskStatus = 'COMPLETED'
|
||||
task.pallets.forEach(pallet => { pallet.status = 'SEALED'; pallet.version += 1 })
|
||||
return response(task)
|
||||
}
|
||||
throw new Error(`Unexpected scenario request: ${method} ${path}`)
|
||||
},
|
||||
}
|
||||
await completeDropshipPalletization(context, { inboundId: '82', inboundCode: 'CODE', warehouseId: '83', measurement: { actualPackageCount: 2 } })
|
||||
return checks
|
||||
}
|
||||
|
||||
test('system-issued pallet HTTP scenario covers count, replay, empty rejection and completed replay', async () => {
|
||||
const checks = await exerciseSystemPalletScenario()
|
||||
assert.ok(checks.length >= 15)
|
||||
assert.deepEqual(checks.filter(check => !check.passed), [])
|
||||
})
|
||||
|
||||
test('system-issued pallet HTTP regression detects duplicate replay and orphan empty pallets', async () => {
|
||||
const replayChecks = await exerciseSystemPalletScenario('duplicate-replay')
|
||||
assert.ok(replayChecks.some(check => check.name.includes('系统发码重放不新增托盘') && !check.passed))
|
||||
const emptyChecks = await exerciseSystemPalletScenario('empty-pallet')
|
||||
assert.ok(emptyChecks.some(check => check.name.includes('拒绝创建没有空托或计数漂移') && !check.passed))
|
||||
})
|
||||
|
||||
@@ -113,7 +113,7 @@ async function waitForCompletedMeasurement(context, measurementPath, expectedSta
|
||||
return { response, measurement, attempts }
|
||||
}
|
||||
|
||||
async function completeDropshipPalletization(context, { inboundId, inboundCode, warehouseId, measurement }) {
|
||||
export async function completeDropshipPalletization(context, { inboundId, inboundCode, warehouseId, measurement }) {
|
||||
const pendingResponse = await context.request('GET',
|
||||
`/api/platform/v1/first-leg/pda-dropship/tasks/pending?keyword=${encodeURIComponent(inboundCode)}`)
|
||||
const pending = context.apiSuccess(MODULE, '查询一件代发待合托任务', pendingResponse,
|
||||
@@ -135,15 +135,31 @@ async function completeDropshipPalletization(context, { inboundId, inboundCode,
|
||||
`taskStatus=${claimed?.taskStatus ?? 'missing'}`, OPTIONS)
|
||||
|
||||
const totalCartons = Number(measurement?.actualPackageCount)
|
||||
const palletDate = new Date().toISOString().slice(2, 10).replace(/-/g, '')
|
||||
let palletHash = 0
|
||||
for (const character of `${context.marker}:${taskId}`) {
|
||||
palletHash = (Math.imul(palletHash, 31) + character.charCodeAt(0)) >>> 0
|
||||
}
|
||||
const palletNo = `DP-${palletDate}-${String(palletHash % 10_000).padStart(4, '0')}`
|
||||
const createPath = `/api/platform/v1/first-leg/pda-dropship/tasks/${taskId}/pallets/create-and-bind`
|
||||
const taskPath = `/api/platform/v1/first-leg/pda-dropship/tasks/${taskId}?warehouseId=${warehouseId}`
|
||||
const createPayload = { warehouseId, operationId: `${context.marker}-dropship-create-bind`.slice(0, 128) }
|
||||
let palletNo = ''
|
||||
let countedCartons = 0
|
||||
let pallet = null
|
||||
|
||||
const palletSnapshot = (value) => JSON.stringify({
|
||||
counted: value?.countedCartons, bound: value?.boundCartons, unbound: value?.unboundCartons,
|
||||
pallets: value?.pallets?.map((item) => ({ id: asId(item.palletId), no: item.palletNo,
|
||||
count: item.cartonCount, status: item.status, version: item.version })),
|
||||
})
|
||||
async function rejectEmptyCreation(before, suffix) {
|
||||
const response = await context.request('POST', createPath,
|
||||
{ ...createPayload, operationId: `${context.marker}-dropship-empty-${suffix}`.slice(0, 128) })
|
||||
context.assert(MODULE, `无待绑箱创建托盘被拒绝-${suffix}`, response,
|
||||
response.http === 200 && String(response.body?.code) === 'A0409',
|
||||
`HTTP=${response.http}, code=${response.body?.code || 'missing'}`, OPTIONS)
|
||||
const afterResponse = await context.request('GET', taskPath)
|
||||
const after = context.apiSuccess(MODULE, `拒绝创建后回查任务-${suffix}`, afterResponse, '', OPTIONS)
|
||||
context.assert(MODULE, `拒绝创建没有空托或计数漂移-${suffix}`, afterResponse,
|
||||
palletSnapshot(after) === palletSnapshot(before), `pallets=${after?.pallets?.length ?? 'missing'}`, OPTIONS)
|
||||
}
|
||||
await rejectEmptyCreation(claimed, 'before-count')
|
||||
|
||||
for (let index = 0; index < totalCartons; index += 1) {
|
||||
const boxCode = `${inboundCode}#${totalCartons}-${index + 1}`
|
||||
const resolveResponse = await context.request('GET',
|
||||
@@ -189,18 +205,21 @@ async function completeDropshipPalletization(context, { inboundId, inboundCode,
|
||||
&& Number(replayed?.unboundCartons) === Number(counted?.unboundCartons),
|
||||
`counted=${replayed?.countedCartons ?? 'missing'}, unbound=${replayed?.unboundCartons ?? 'missing'}`, OPTIONS)
|
||||
|
||||
const bindResponse = await context.request('POST',
|
||||
`/api/platform/v1/first-leg/pda-dropship/tasks/${taskId}/pallets/bind-pending`, {
|
||||
warehouseId, palletNo,
|
||||
operationId: `${context.marker}-dropship-bind-pending`.slice(0, 128),
|
||||
})
|
||||
const bound = context.apiSuccess(MODULE, '扫描托盘并绑定全部待组托箱', bindResponse,
|
||||
`taskId=${taskId}, palletNo=${palletNo}`, OPTIONS)
|
||||
pallet = bound?.pallets?.find((item) => item?.palletNo === palletNo) || null
|
||||
const bindResponse = await context.request('POST', createPath, createPayload)
|
||||
const bound = context.apiSuccess(MODULE, '系统发码创建托盘并绑定全部待组托箱', bindResponse,
|
||||
`taskId=${taskId}`, OPTIONS)
|
||||
pallet = bound?.pallets?.find((item) => Number(item.cartonCount) === 1) || null
|
||||
palletNo = pallet?.palletNo || ''
|
||||
context.assert(MODULE, '待组托箱绑定到OPEN托盘且不再游离', bindResponse,
|
||||
Number(bound?.unboundCartons) === 0 && Number(pallet?.cartonCount) === 1
|
||||
&& pallet?.status === 'OPEN',
|
||||
&& pallet?.status === 'OPEN' && /^DP-\d{6}-\d{4}$/.test(palletNo)
|
||||
&& bound?.pallets?.length === 1,
|
||||
`unbound=${bound?.unboundCartons ?? 'missing'}, palletStatus=${pallet?.status ?? 'missing'}`, OPTIONS)
|
||||
const createReplayResponse = await context.request('POST', createPath, createPayload)
|
||||
const createReplay = context.apiSuccess(MODULE, '系统发码同operationId重放', createReplayResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '系统发码重放不新增托盘且业务码和计数稳定', createReplayResponse,
|
||||
palletSnapshot(createReplay) === palletSnapshot(bound), `palletNo=${palletNo}`, OPTIONS)
|
||||
await rejectEmptyCreation(bound, 'after-bind')
|
||||
} else {
|
||||
pallet = counted?.pallet || pallet
|
||||
context.assert(MODULE, `第${index + 1}箱直接进入当前OPEN托盘`, countResponse,
|
||||
@@ -229,6 +248,11 @@ async function completeDropshipPalletization(context, { inboundId, inboundCode,
|
||||
&& Number(sealed?.unboundCartons) === 0
|
||||
&& sealed?.pallets?.every((item) => item?.status === 'SEALED'),
|
||||
`taskStatus=${sealed?.taskStatus ?? 'missing'}, counted=${sealed?.countedCartons ?? 'missing'}, bound=${sealed?.boundCartons ?? 'missing'}`, OPTIONS)
|
||||
const sealedReplayResponse = await context.request('POST', createPath, createPayload)
|
||||
const sealedReplay = context.apiSuccess(MODULE, '封托完成后系统发码原操作仍可重放', sealedReplayResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '完成后重放不重开任务或新建空托', sealedReplayResponse,
|
||||
sealedReplay?.taskStatus === 'COMPLETED' && palletSnapshot(sealedReplay) === palletSnapshot(sealed),
|
||||
`taskStatus=${sealedReplay?.taskStatus || 'missing'}, palletNo=${palletNo}`, OPTIONS)
|
||||
return sealed
|
||||
}
|
||||
|
||||
|
||||
@@ -86,8 +86,8 @@ export async function run(context) {
|
||||
&& !rows(operations).some((item) => ['REVIEW', 'COMPLETE_LOADING'].includes(item.actionType)),
|
||||
`actions=${rows(operations).map((item) => item.actionType).join(',') || 'none'}`, OPTIONS)
|
||||
|
||||
const containerResponse = await context.request('GET', `${CONTAINERS}/${encodeURIComponent(source.containerBookingId)}`)
|
||||
const container = context.apiSuccess(MODULE, '最终完成前回查 RELEASED 订舱柜次', containerResponse, '', OPTIONS)
|
||||
const containerResponse = await context.request('GET', `${CONTAINERS}/domestic/${encodeURIComponent(source.containerBookingId)}`)
|
||||
const container = context.apiSuccess(MODULE, '最终完成前回查 RELEASED 订舱柜次', containerResponse, '', OPTIONS)?.container
|
||||
const bookingResponse = container?.oceanBookingId
|
||||
? await context.request('GET', `/api/platform/v1/ocean-bookings/${encodeURIComponent(container.oceanBookingId)}`)
|
||||
: containerResponse
|
||||
|
||||
@@ -41,8 +41,8 @@ export async function run(context) {
|
||||
OPTIONS)
|
||||
if (!bookingId) return
|
||||
|
||||
const containerResponse = await context.request('GET', `${CONTAINERS}/${encodeURIComponent(bookingId)}`)
|
||||
const container = context.apiSuccess(MODULE, '读取人工分柜关联柜次', containerResponse, '', OPTIONS)
|
||||
const containerResponse = await context.request('GET', `${CONTAINERS}/domestic/${encodeURIComponent(bookingId)}`)
|
||||
const container = context.apiSuccess(MODULE, '读取人工分柜关联柜次', containerResponse, '', OPTIONS)?.container
|
||||
context.assert(MODULE, '柜次保持准备清单的仓库和渠道范围', containerResponse,
|
||||
id(container?.id) === bookingId && id(container?.warehouseId) === id(manifest.warehouseId)
|
||||
&& container?.channelCode === manifest.channelCode,
|
||||
|
||||
@@ -46,8 +46,8 @@ export async function run(context) {
|
||||
OPTIONS)
|
||||
if (!plan || beforeItems.length === 0) return
|
||||
|
||||
const containerResponse = await context.request('GET', `${CONTAINERS}/${encodeURIComponent(containerId)}`)
|
||||
const container = context.apiSuccess(MODULE, 'PDA 就绪操作前读取内部模拟柜', containerResponse, '', OPTIONS)
|
||||
const containerResponse = await context.request('GET', `${CONTAINERS}/domestic/${encodeURIComponent(containerId)}`)
|
||||
const container = context.apiSuccess(MODULE, 'PDA 就绪操作前读取内部模拟柜', containerResponse, '', OPTIONS)?.container
|
||||
context.assert(MODULE, '操作前柜次仍无真实柜号和订舱关联', containerResponse,
|
||||
id(container?.id) === containerId && container?.cabinetNo === source.cabinetNo
|
||||
&& container?.containerNo == null && container?.oceanBookingId == null
|
||||
|
||||
@@ -47,8 +47,8 @@ export async function run(context) {
|
||||
`status=${plan?.status || 'missing'}, beforeVersion=${fixture.beforePlanVersion}, afterVersion=${plan?.version ?? 'missing'}, items=${afterItems.length}`,
|
||||
OPTIONS)
|
||||
|
||||
const containerResponse = await context.request('GET', `${CONTAINERS}/${encodeURIComponent(containerId)}`)
|
||||
const container = context.apiSuccess(MODULE, '读取关联真实柜号后的稳定柜次', containerResponse, '', OPTIONS)
|
||||
const containerResponse = await context.request('GET', `${CONTAINERS}/domestic/${encodeURIComponent(containerId)}`)
|
||||
const container = context.apiSuccess(MODULE, '读取关联真实柜号后的稳定柜次', containerResponse, '', OPTIONS)?.container
|
||||
const oceanBookingId = id(container?.oceanBookingId)
|
||||
const realContainerNo = String(container?.containerNo || '')
|
||||
context.assert(MODULE, '真实柜号和订舱原地补全且内部柜次身份不变', containerResponse,
|
||||
|
||||
@@ -47,8 +47,8 @@ export async function run(context) {
|
||||
context.assert(MODULE, '未点击推送前不存在预装 bridge', linksResponse,
|
||||
rows(links).length === 0, `linkCount=${rows(links).length}`, OPTIONS)
|
||||
|
||||
const containerResponse = await context.request('GET', `${CONTAINERS}/${encodeURIComponent(containerId)}`)
|
||||
const container = context.apiSuccess(MODULE, '推送前读取内部模拟柜', containerResponse, '', OPTIONS)
|
||||
const containerResponse = await context.request('GET', `${CONTAINERS}/domestic/${encodeURIComponent(containerId)}`)
|
||||
const container = context.apiSuccess(MODULE, '推送前读取内部模拟柜', containerResponse, '', OPTIONS)?.container
|
||||
context.assert(MODULE, '当前仍是无真实柜号且未关联订舱的内部模拟柜', containerResponse,
|
||||
id(container?.id) === containerId && container?.containerNo == null && container?.oceanBookingId == null
|
||||
&& id(container?.warehouseId) === id(source.warehouseId) && container?.channelCode === source.channelCode,
|
||||
|
||||
@@ -87,8 +87,8 @@ export async function run(context) {
|
||||
`HTTP=${taskResponse.http}, code=${codeOf(taskResponse)}, requestId=${taskResponse.requestId || 'missing'}, traceId=${taskResponse.traceId || 'missing'}`,
|
||||
OPTIONS)
|
||||
|
||||
const containerResponse = await context.request('GET', `${CONTAINERS}/${encodeURIComponent(containerId)}`)
|
||||
const container = context.apiSuccess(MODULE, '回查推送后内部柜次身份', containerResponse, '', OPTIONS)
|
||||
const containerResponse = await context.request('GET', `${CONTAINERS}/domestic/${encodeURIComponent(containerId)}`)
|
||||
const container = context.apiSuccess(MODULE, '回查推送后内部柜次身份', containerResponse, '', OPTIONS)?.container
|
||||
context.assert(MODULE, '推送预装不伪造真实柜号或订舱关联', containerResponse,
|
||||
id(container?.id) === containerId && container?.cabinetNo === fixture.cabinetNo
|
||||
&& container?.containerNo == null && container?.oceanBookingId == null,
|
||||
|
||||
@@ -316,10 +316,10 @@ async function complete(context, chain, label) {
|
||||
}
|
||||
|
||||
async function verifyContainer(context, status, fixture) {
|
||||
const response = await context.request('GET', `${CONTAINERS}?status=${status}&page=1&size=100`)
|
||||
const response = await context.request('GET', `${CONTAINERS}/domestic?scope=ACTIVE&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),
|
||||
records(page).some((item) => id(item.container?.id) === fixture.id && item.container?.planStatus === status),
|
||||
`id=${fixture.id}`, OPTIONS)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,18 @@ function failure(response) {
|
||||
|| (response?.http === 200 && codeOf(response) !== '200')
|
||||
}
|
||||
function records(page) { return Array.isArray(page?.records) ? page.records : [] }
|
||||
function containerRecord(value) { return value?.container || value }
|
||||
function containerRecord(value) { return value?.container }
|
||||
|
||||
export function containerWorkbenchGaps(value) {
|
||||
return [
|
||||
!value?.container?.id && 'container.id',
|
||||
!value?.container?.cabinetNo && 'container.cabinetNo',
|
||||
!Number.isInteger(value?.container?.version) && 'container.version',
|
||||
!value?.workbenchStage && 'workbenchStage',
|
||||
(!value?.statusSummary || typeof value.statusSummary !== 'object') && 'statusSummary',
|
||||
!Array.isArray(value?.allowedActions) && 'allowedActions',
|
||||
].filter(Boolean)
|
||||
}
|
||||
|
||||
async function waitForCompletedTask(context, taskId, warehouseId, attempts = 45) {
|
||||
let response = null
|
||||
@@ -262,6 +273,18 @@ export async function run(context) {
|
||||
|
||||
const containerDetailResponse = await context.request('GET', `${DOMESTIC_CONTAINERS}/${containerId}`)
|
||||
const containerDetail = containerRecord(context.apiSuccess(MODULE, '回查货柜详情', containerDetailResponse, '', OPTIONS))
|
||||
const workbenchGaps = containerWorkbenchGaps(containerDetailResponse.body?.data)
|
||||
context.assert(MODULE, '货柜详情返回正式工作台 DTO 而非旧扁平实体', containerDetailResponse,
|
||||
workbenchGaps.length === 0, `missing=${workbenchGaps.join('|') || 'none'}`, OPTIONS)
|
||||
const pageResponse = await context.request('GET',
|
||||
`${DOMESTIC_CONTAINERS}?page=1&size=20&scope=ACTIVE&status=${encodeURIComponent(containerDetail?.planStatus || '')}&keyword=${encodeURIComponent(containerDetail?.cabinetNo || '')}`)
|
||||
const page = context.apiSuccess(MODULE, '按货柜页面筛选参数查询本轮模拟柜', pageResponse, '', OPTIONS)
|
||||
const ownRows = records(page).filter((item) => id(item?.container?.id) === containerId)
|
||||
context.assert(MODULE, '货柜页面列表返回唯一模拟柜和工作台动作状态', pageResponse,
|
||||
ownRows.length === 1 && records(page).every((item) => containerWorkbenchGaps(item).length === 0)
|
||||
&& ownRows[0]?.container?.cabinetNo === containerDetail?.cabinetNo
|
||||
&& ownRows[0]?.container?.planStatus === containerDetail?.planStatus,
|
||||
`matches=${ownRows.length}, rows=${records(page).length}`, OPTIONS)
|
||||
context.assert(MODULE, '货柜详情保留候选货物路线约束', containerDetailResponse,
|
||||
(!containerDetail?.destinationWarehouseCode || !candidate.destinationWarehouseCode
|
||||
|| containerDetail.destinationWarehouseCode === candidate.destinationWarehouseCode)
|
||||
@@ -475,7 +498,7 @@ export async function run(context) {
|
||||
const activeContainersResponse = await context.request('GET',
|
||||
`${DOMESTIC_CONTAINERS}?page=1&size=50&scope=ACTIVE&keyword=${encodeURIComponent(realContainerNo)}`)
|
||||
const activeContainers = context.apiSuccess(MODULE, '关联后按真实柜号回查正常货柜列表', activeContainersResponse, '', OPTIONS)
|
||||
const activeRealRows = records(activeContainers).map(containerRecord).filter((item) => item.containerNo === realContainerNo)
|
||||
const activeRealRows = records(activeContainers).map(containerRecord).filter((item) => item?.containerNo === realContainerNo)
|
||||
context.assert(MODULE, '关联后列表仅保留模拟柜原地补全的一条 ACTIVE 操作身份', activeContainersResponse,
|
||||
activeRealRows.length === 1 && id(activeRealRows[0]?.id) === containerId,
|
||||
`activeIds=${activeRealRows.map((item) => id(item?.id)).join('|') || 'none'}`, OPTIONS)
|
||||
@@ -592,7 +615,7 @@ export async function run(context) {
|
||||
if (!taskId || !pickItem?.id) return
|
||||
|
||||
const releasedContainerResponse = await context.request('GET', `${DOMESTIC_CONTAINERS}/${containerId}`)
|
||||
const releasedContainer = context.apiSuccess(MODULE, '任务生成后回查货柜运输版本', releasedContainerResponse, '', OPTIONS)
|
||||
const releasedContainer = containerRecord(context.apiSuccess(MODULE, '任务生成后回查货柜运输版本', releasedContainerResponse, '', OPTIONS))
|
||||
|
||||
const stableIdentity = {
|
||||
taskId, loadingPlanId: id(task.loadingPlanId), containerBookingId: id(task.containerBookingId),
|
||||
|
||||
@@ -26,7 +26,8 @@ const matrix = read('../../../docs/国内货柜履约/货柜管理全链验收
|
||||
|
||||
test('container acceptance reads the canonical domestic workbench route', () => {
|
||||
assert.match(containerLoadingAcceptance, /const DOMESTIC_CONTAINERS = `\$\{CONTAINERS\}\/domestic`/)
|
||||
assert.match(containerLoadingAcceptance, /function containerRecord\(value\) \{ return value\?\.container \|\| value \}/)
|
||||
assert.match(containerLoadingAcceptance, /function containerRecord\(value\) \{ return value\?\.container \}/)
|
||||
assert.doesNotMatch(containerLoadingAcceptance, /value\?\.container \|\| value/)
|
||||
assert.match(containerLoadingAcceptance, /DOMESTIC_CONTAINERS\}\?page=1&size=50&scope=ACTIVE&keyword=/)
|
||||
assert.doesNotMatch(containerLoadingAcceptance, /request\('GET', `\$\{CONTAINERS\}\/\$\{containerId\}`\)/)
|
||||
assert.match(phase3LoadingAcceptance, /\/api\/platform\/v1\/containers\/domestic\/\$\{containerBookingId\}/)
|
||||
|
||||
@@ -25,7 +25,7 @@ function failure(response) {
|
||||
return [400, 401, 403, 404, 409, 503].includes(response?.http)
|
||||
|| (response?.http === 200 && codeOf(response) !== '200')
|
||||
}
|
||||
function containerRecord(value) { return value?.container || value }
|
||||
function containerRecord(value) { return value?.container }
|
||||
|
||||
async function ensureMatsonSupplier(context, records) {
|
||||
const existing = records.find((value) => value?.id && value?.name === MATSON_SUPPLIER_NAME)
|
||||
|
||||
@@ -79,7 +79,8 @@ test('container API uses one stable long string identity', () => {
|
||||
assert.match(endpoint, /sourceContainerBookingId:\s*\{ type: string/)
|
||||
assert.match(controller, /convertContainer/)
|
||||
assert.match(adminApi, /sourceContainerBookingId: string/)
|
||||
assert.match(adminView, /sourceContainerBookingId: container\.sourceContainerBookingId/)
|
||||
assert.match(adminView, /const bookingId = preview\.sourceContainerBookingId/)
|
||||
assert.match(adminView, /convertLastMileContainerToInboundPlans\(\{\s*sourceContainerBookingId: bookingId,\s*snapshotVersion: preview\.snapshotVersion,\s*snapshotHash: preview\.snapshotHash,/)
|
||||
})
|
||||
|
||||
test('backend batch conversion is one transaction and never merges same-customer orders', () => {
|
||||
|
||||
Reference in New Issue
Block a user