Files
baiyue-firstleg/scripts/admin-test-env-modules/phase4-overseas-warehouse.mjs
T
2026-08-21 10:38:28 +08:00

243 lines
18 KiB
JavaScript

const MODULE = 'phase4-overseas-warehouse'
const SCENARIO = 'phase4-overseas-warehouse'
const OPTIONS = { scenario: SCENARIO, deploymentBlocker: true }
function id(value) { return value == null ? '' : String(value) }
function nonBlank(value) { return typeof value === 'string' && value.trim().length > 0 }
function nonNegative(value) { return Number.isFinite(Number(value)) && Number(value) >= 0 }
function longId(value) { return /^\d+$/.test(id(value)) && id(value) !== '0' }
function trace(response) {
return `HTTP=${response?.http ?? 'missing'}, code=${response?.body?.code ?? 'missing'}, requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}`
}
function query(path, params) {
const search = new URLSearchParams()
for (const [key, value] of Object.entries(params)) if (value !== undefined && value !== null && value !== '') search.set(key, String(value))
return `${path}?${search}`
}
function records(page) { return Array.isArray(page?.records) ? page.records : [] }
function pageValid(page) {
return Array.isArray(page?.records) && Number.isInteger(Number(page?.page)) && Number(page.page) >= 1
&& Number.isInteger(Number(page?.size)) && Number(page.size) >= 1
&& nonNegative(page?.total) && nonNegative(page?.pages)
}
function assertEnvelope(context, name, response) {
context.assert(MODULE, `${name}响应包络正确`, response,
response?.http === 200 && String(response?.body?.code) === '200' && Object.hasOwn(response?.body || {}, 'data'),
trace(response), OPTIONS)
}
async function domesticGet(context, name, path) {
const response = await context.request('GET', path)
assertEnvelope(context, name, response)
return { response, data: context.apiSuccess(MODULE, name, response, trace(response), OPTIONS) }
}
async function overseasGet(context, name, path) {
const response = await context.requestOverseas('GET', path)
assertEnvelope(context, name, response)
return { response, data: context.apiSuccess(MODULE, name, response, trace(response), OPTIONS) }
}
async function overseasMutation(context, method, name, path, body) {
const response = await context.requestOverseas(method, path, body)
assertEnvelope(context, name, response)
return { response, data: context.apiSuccess(MODULE, name, response, trace(response), OPTIONS) }
}
async function overseasCsv(context, name, path) {
const response = await context.requestOverseasBinary('GET', path)
context.addStep(MODULE, name, response,
response?.http === 200 && Number(response?.bytes?.length) > 0 && String(response?.contentType || '').toLowerCase().includes('csv'),
`bytes=${response?.bytes?.length ?? 0}, contentType=${response?.contentType || 'missing'}, requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}`, OPTIONS)
return response
}
function assertPage(context, name, result) {
context.assert(MODULE, `${name}分页结构正确`, result.response, pageValid(result.data),
`records=${result.data?.records?.length ?? 'missing'}, page=${result.data?.page ?? 'missing'}, size=${result.data?.size ?? 'missing'}, total=${result.data?.total ?? 'missing'}; ${trace(result.response)}`, OPTIONS)
}
function coverageGap(context, name, reason, category = 'prerequisite') {
context.addCoverageGap(MODULE, SCENARIO, name, reason, category)
}
export async function run(context) {
context.createdResources.phase4OverseasWarehouse = { marker: context.marker, customerId: null, warehouseId: null, palletId: null, terminalStatus: null }
const tenantsResult = await domesticGet(context, '查询可用客户候选', '/api/platform/v1/iam/tenants?page=1&size=100&status=ACTIVE')
assertPage(context, '可用客户候选', tenantsResult)
const customer = records(tenantsResult.data).find((item) => longId(item?.id) && item?.status === 'ACTIVE')
const customerId = id(customer?.id)
if (!customerId) {
coverageGap(context, '海外只读接口显式客户范围', `IAM ACTIVE 客户列表未返回可用 customerId;${trace(tenantsResult.response)}`)
return
}
const scoped = { customerId, page: 1, size: 20 }
const orders = await overseasGet(context, '查询尾程订单列表', query('/api/platform/v1/last-leg/orders', scoped))
assertPage(context, '尾程订单列表', orders)
const order = records(orders.data)[0]
if (order) {
context.assert(MODULE, '尾程订单关键字段正确', orders.response,
longId(order.id) && nonBlank(order.orderNo) && nonBlank(order.currentStatus),
`id=${id(order.id) || 'missing'}, orderNo=${order.orderNo || 'missing'}, status=${order.currentStatus || 'missing'}; ${trace(orders.response)}`, OPTIONS)
const detail = await overseasGet(context, '查询尾程订单详情', query(`/api/platform/v1/last-leg/orders/${encodeURIComponent(id(order.id))}`, { customerId }))
context.assert(MODULE, '尾程订单详情对应列表记录', detail.response,
id(detail.data?.order?.id) === id(order.id) && Array.isArray(detail.data?.cargo) && Array.isArray(detail.data?.statusHistory),
`listId=${id(order.id)}, detailId=${id(detail.data?.order?.id) || 'missing'}; ${trace(detail.response)}`, OPTIONS)
}
const relabels = await overseasGet(context, '查询换标单列表', query('/api/platform/v1/last-leg/relabel-applications', scoped))
assertPage(context, '换标单列表', relabels)
const relabel = records(relabels.data)[0]
if (relabel) {
context.assert(MODULE, '换标单关键字段正确', relabels.response,
longId(relabel.id) && nonBlank(relabel.relabelNo) && nonBlank(relabel.status),
`id=${id(relabel.id) || 'missing'}, relabelNo=${relabel.relabelNo || 'missing'}, status=${relabel.status || 'missing'}; ${trace(relabels.response)}`, OPTIONS)
const detail = await overseasGet(context, '查询换标单详情', query(`/api/platform/v1/last-leg/relabel-applications/${encodeURIComponent(relabel.relabelNo)}`, { customerId }))
context.assert(MODULE, '换标单详情对应列表记录', detail.response,
detail.data?.application?.relabelNo === relabel.relabelNo && Array.isArray(detail.data?.items) && Array.isArray(detail.data?.history),
`listNo=${relabel.relabelNo}, detailNo=${detail.data?.application?.relabelNo || 'missing'}; ${trace(detail.response)}`, OPTIONS)
}
const plans = await overseasGet(context, '查询入仓计划列表', query('/api/platform/v1/last-leg/inbound-plans', scoped))
assertPage(context, '入仓计划列表', plans)
const plan = records(plans.data)[0]
if (plan) {
context.assert(MODULE, '入仓计划关键字段正确', plans.response,
longId(plan.id) && longId(plan.sourceOrderId) && nonBlank(plan.planNo) && id(plan.customerId) === customerId && nonBlank(plan.status),
`id=${id(plan.id) || 'missing'}, sourceOrderId=${id(plan.sourceOrderId) || 'missing'}, planNo=${plan.planNo || 'missing'}, status=${plan.status || 'missing'}; ${trace(plans.response)}`, OPTIONS)
const detail = await overseasGet(context, '查询入仓计划详情', query(`/api/platform/v1/last-leg/inbound-plans/${encodeURIComponent(plan.planNo)}`, { customerId }))
context.assert(MODULE, '入仓计划详情对应列表记录', detail.response,
detail.data?.planNo === plan.planNo && longId(detail.data?.sourceOrderId)
&& Array.isArray(detail.data?.lines) && Array.isArray(detail.data?.pallets) && Array.isArray(detail.data?.allocations),
`listNo=${plan.planNo}, detailNo=${detail.data?.planNo || 'missing'}, sourceOrderId=${id(detail.data?.sourceOrderId) || 'missing'}; ${trace(detail.response)}`, OPTIONS)
}
const summary = await overseasGet(context, '查询库存汇总', query('/api/platform/v1/last-leg/inventory/summary', { customerId, page: 1, size: 200 }))
context.assert(MODULE, '库存汇总关键数量非负', summary.response,
['inboundOrderCount', 'palletCount', 'totalPackages', 'availablePackages', 'holdPackages', 'inTransitPackages', 'receivingPackages', 'procurementPackages']
.every((field) => nonNegative(summary.data?.[field])),
`total=${summary.data?.totalPackages ?? 'missing'}, available=${summary.data?.availablePackages ?? 'missing'}; ${trace(summary.response)}`, OPTIONS)
const balances = await overseasGet(context, '查询SKU库存余额', query('/api/platform/v1/last-leg/inventory/balances', scoped))
assertPage(context, 'SKU库存余额', balances)
const balance = records(balances.data)[0]
if (balance) {
context.assert(MODULE, 'SKU库存余额关键字段正确', balances.response,
longId(balance.id) && id(balance.customerId) === customerId && nonBlank(balance.skuCode)
&& nonNegative(balance.totalPackages) && nonNegative(balance.availablePackages) && nonBlank(balance.updatedAt),
`id=${id(balance.id) || 'missing'}, sku=${balance.skuCode || 'missing'}, total=${balance.totalPackages ?? 'missing'}; ${trace(balances.response)}`, OPTIONS)
const positions = await overseasGet(context, '查询库存库位详情', query(`/api/platform/v1/last-leg/inventory/balances/${encodeURIComponent(id(balance.id))}/positions`, { customerId }))
context.assert(MODULE, '库存库位详情结构正确', positions.response,
Array.isArray(positions.data) && positions.data.every((item) => longId(item?.id) && id(item?.balanceId) === id(balance.id) && nonNegative(item?.totalPackages)),
`balanceId=${id(balance.id)}, positions=${positions.data?.length ?? 'missing'}; ${trace(positions.response)}`, OPTIONS)
}
const ledger = await overseasGet(context, '查询进销存流水', query('/api/platform/v1/last-leg/inventory/ledger', scoped))
assertPage(context, '进销存流水', ledger)
const ledgerRow = records(ledger.data)[0]
if (ledgerRow) context.assert(MODULE, '进销存流水关键字段正确', ledger.response,
longId(ledgerRow.id) && id(ledgerRow.customerId) === customerId && nonBlank(ledgerRow.businessType)
&& Number.isFinite(Number(ledgerRow.beforePackages)) && Number.isFinite(Number(ledgerRow.deltaPackages))
&& Number.isFinite(Number(ledgerRow.afterPackages)) && nonBlank(ledgerRow.occurredAt),
`id=${id(ledgerRow.id) || 'missing'}, type=${ledgerRow.businessType || 'missing'}, delta=${ledgerRow.deltaPackages ?? 'missing'}; ${trace(ledger.response)}`, OPTIONS)
await overseasCsv(context, '下载SKU库存余额CSV', query('/api/platform/v1/last-leg/inventory/balances/export', { customerId, page: 1, size: 200 }))
await overseasCsv(context, '下载进销存流水CSV', query('/api/platform/v1/last-leg/inventory/ledger/export', { customerId, page: 1, size: 200 }))
coverageGap(context, '换标创建与状态生命周期',
'当前 Admin 换标新增/审核仍使用不可提交的契约提示弹框,前端未真实接入写 API;验收模块不越过页面范围直接创建换标单', 'frontend-scope')
const warehouse = await findWarehouse(context, records(tenantsResult.data), customerId)
if (!warehouse) return
const warehouseId = id(warehouse.id)
context.createdResources.phase4OverseasWarehouse.customerId = customerId
context.createdResources.phase4OverseasWarehouse.warehouseId = warehouseId
const pallets = await overseasGet(context, '查询公共托盘列表', query(`/api/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets`, { page: 1, size: 20 }))
assertPage(context, '公共托盘列表', pallets)
const pallet = records(pallets.data)[0]
if (pallet) {
context.assert(MODULE, '公共托盘关键字段正确', pallets.response,
longId(pallet.id) && id(pallet.warehouseId) === warehouseId && id(pallet.customerId) === '0'
&& nonBlank(pallet.palletCode) && ['IDLE', 'OCCUPIED', 'SCRAPPED'].includes(pallet.status),
`id=${id(pallet.id) || 'missing'}, customerId=${id(pallet.customerId) || 'missing'}, status=${pallet.status || 'missing'}; ${trace(pallets.response)}`, OPTIONS)
const detail = await overseasGet(context, '查询公共托盘详情', `/api/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets/${encodeURIComponent(id(pallet.id))}`)
context.assert(MODULE, '公共托盘详情对应列表记录', detail.response,
id(detail.data?.id) === id(pallet.id) && id(detail.data?.warehouseId) === warehouseId && id(detail.data?.customerId) === '0',
`listId=${id(pallet.id)}, detailId=${id(detail.data?.id) || 'missing'}; ${trace(detail.response)}`, OPTIONS)
}
await verifyPalletLifecycle(context, warehouseId)
context.context.phase4OverseasWarehouse = { customerId, warehouseId, palletId: context.createdResources.phase4OverseasWarehouse.palletId }
}
async function verifyPalletLifecycle(context, warehouseId) {
const marker = String(context.marker || '').replace(/[^A-Za-z0-9_-]/g, '').slice(-48) || 'PHASE4'
const palletCode = `ACC-${marker}`.slice(0, 128)
let palletId = ''
try {
const created = await overseasMutation(context, 'POST', '创建本轮公共托盘',
`/api/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets`, {
palletCode, palletType: '木托盘', capacityCbm: 2, unitPrice: 0, status: 'IDLE', remark: context.marker,
})
palletId = id(created.data)
context.createdResources.phase4OverseasWarehouse.palletId = palletId || null
context.assert(MODULE, '公共托盘创建返回长ID', created.response, longId(palletId),
`palletId=${palletId || 'missing'}, marker=${context.marker}; ${trace(created.response)}`, OPTIONS)
if (!palletId) return
const detailPath = `/api/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets/${encodeURIComponent(palletId)}`
const detail = await overseasGet(context, '查询本轮公共托盘详情', detailPath)
context.assert(MODULE, '本轮公共托盘创建事实正确', detail.response,
id(detail.data?.id) === palletId && id(detail.data?.warehouseId) === warehouseId
&& id(detail.data?.customerId) === '0' && detail.data?.palletCode === palletCode && detail.data?.status === 'IDLE',
`palletId=${id(detail.data?.id) || 'missing'}, code=${detail.data?.palletCode || 'missing'}, customerId=${id(detail.data?.customerId) || 'missing'}; ${trace(detail.response)}`, OPTIONS)
const updated = await overseasMutation(context, 'PUT', '编辑本轮公共托盘', detailPath, {
palletCode, palletType: '木托盘', capacityCbm: 2.5, unitPrice: 1, status: 'IDLE',
locationId: id(detail.data?.locationId) || undefined, remark: `${context.marker}-edited`,
})
context.assert(MODULE, '公共托盘编辑成功', updated.response, updated.data === null,
`palletId=${palletId}; ${trace(updated.response)}`, OPTIONS)
const after = await overseasGet(context, '回查公共托盘编辑结果', detailPath)
context.assert(MODULE, '公共托盘编辑字段已生效', after.response,
id(after.data?.id) === palletId && Number(after.data?.capacityCbm) === 2.5
&& Number(after.data?.unitPrice) === 1 && after.data?.remark === `${context.marker}-edited`,
`capacity=${after.data?.capacityCbm ?? 'missing'}, unitPrice=${after.data?.unitPrice ?? 'missing'}, remark=${after.data?.remark || 'missing'}; ${trace(after.response)}`, OPTIONS)
coverageGap(context, '公共托盘旧版本并发冲突',
'当前 Pallet SaveRequest/PUT 未提供 version 或 expectedVersion,无法构造稳定旧版本冲突且禁止伪造冲突请求', 'missing-version-contract')
} finally {
if (palletId) {
const retired = await overseasMutation(context, 'PUT', '报废本轮公共托盘',
`/api/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets/${encodeURIComponent(palletId)}`, {
palletCode, palletType: '木托盘', capacityCbm: 2.5, unitPrice: 1, status: 'SCRAPPED',
remark: `${context.marker}-acceptance-retired`,
})
context.createdResources.phase4OverseasWarehouse.terminalStatus = retired.data === null ? 'SCRAPPED' : null
context.assert(MODULE, '本轮公共托盘已报废收口', retired.response, retired.data === null,
`palletId=${palletId}, terminalStatus=${context.createdResources.phase4OverseasWarehouse.terminalStatus || 'missing'}; ${trace(retired.response)}`, OPTIONS)
const retiredDetail = await overseasGet(context, '回查本轮公共托盘报废状态',
`/api/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets/${encodeURIComponent(palletId)}`)
context.assert(MODULE, '本轮公共托盘终态为已报废', retiredDetail.response,
id(retiredDetail.data?.id) === palletId && retiredDetail.data?.status === 'SCRAPPED',
`palletId=${id(retiredDetail.data?.id) || 'missing'}, status=${retiredDetail.data?.status || 'missing'}; ${trace(retiredDetail.response)}`, OPTIONS)
}
}
}
async function findWarehouse(context, tenants, preferredCustomerId) {
const candidates = [...new Set([preferredCustomerId, ...tenants.map((item) => id(item?.id)).filter(longId)])]
let lastResponse = null
for (const customerId of candidates) {
const result = await overseasGet(context, `查询客户${customerId}可用海外仓`, query('/api/platform/v1/warehouse-admin', { customerId, page: 1, size: 100, status: 'ACTIVE' }))
assertPage(context, `客户${customerId}可用海外仓`, result)
lastResponse = result.response
const warehouse = records(result.data).find((item) => longId(item?.id) && item?.status === 'ACTIVE')
if (warehouse) {
context.assert(MODULE, '海外仓关键字段正确', result.response,
nonBlank(warehouse.warehouseCode) && nonBlank(warehouse.warehouseName),
`warehouseId=${id(warehouse.id)}, code=${warehouse.warehouseCode || 'missing'}; ${trace(result.response)}`, OPTIONS)
const detail = await overseasGet(context, '查询海外仓详情', query(`/api/platform/v1/warehouse-admin/${encodeURIComponent(id(warehouse.id))}`, { customerId }))
context.assert(MODULE, '海外仓详情对应列表记录', detail.response,
id(detail.data?.warehouse?.id || detail.data?.id) === id(warehouse.id),
`listId=${id(warehouse.id)}, detailId=${id(detail.data?.warehouse?.id || detail.data?.id) || 'missing'}; ${trace(detail.response)}`, OPTIONS)
return warehouse
}
}
coverageGap(context, '公共托盘真实仓库前置', `ACTIVE 客户列表未找到可用海外仓;${trace(lastResponse)}`, 'missing-readonly-warehouse')
return null
}