test: strengthen overseas warehouse PDA contracts
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
const MODULE = 'phase4-overseas-warehouse'
|
||||
const SCENARIO = 'phase4-overseas-warehouse'
|
||||
const OPTIONS = { scenario: SCENARIO, deploymentBlocker: true }
|
||||
const OPTIONS = { scenario: SCENARIO }
|
||||
|
||||
function id(value) { return value == null ? '' : String(value) }
|
||||
function nonBlank(value) { return typeof value === 'string' && value.trim().length > 0 }
|
||||
@@ -52,8 +52,8 @@ 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)
|
||||
function businessConflict(response) {
|
||||
return response?.http === 409 || (response?.http === 200 && String(response?.body?.code) === 'A0409')
|
||||
}
|
||||
|
||||
export async function run(context) {
|
||||
@@ -63,7 +63,8 @@ export async function run(context) {
|
||||
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)}`)
|
||||
context.assert(MODULE, '海外仓验收必须取得ACTIVE客户', tenantsResult.response, false,
|
||||
`IAM ACTIVE 客户列表未返回可用 customerId;${trace(tenantsResult.response)}`, OPTIONS)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -108,7 +109,7 @@ export async function run(context) {
|
||||
`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 }))
|
||||
const summary = await overseasGet(context, '查询库存汇总(不传分页参数)', query('/api/platform/v1/last-leg/inventory/summary', { customerId }))
|
||||
context.assert(MODULE, '库存汇总关键数量非负', summary.response,
|
||||
['inboundOrderCount', 'palletCount', 'totalPackages', 'availablePackages', 'holdPackages', 'inTransitPackages', 'receivingPackages', 'procurementPackages']
|
||||
.every((field) => nonNegative(summary.data?.[field])),
|
||||
@@ -136,11 +137,8 @@ export async function run(context) {
|
||||
&& 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')
|
||||
await overseasCsv(context, '下载SKU库存余额CSV(不传分页参数)', query('/api/platform/v1/last-leg/inventory/balances/export', { customerId }))
|
||||
await overseasCsv(context, '下载进销存流水CSV(不传分页参数)', query('/api/platform/v1/last-leg/inventory/ledger/export', { customerId }))
|
||||
|
||||
let warehouse = await findWarehouse(context, records(tenantsResult.data), customerId)
|
||||
let ownedWarehouse = false
|
||||
@@ -152,6 +150,19 @@ export async function run(context) {
|
||||
const warehouseId = id(warehouse.id)
|
||||
context.createdResources.phase4OverseasWarehouse.customerId = customerId
|
||||
context.createdResources.phase4OverseasWarehouse.warehouseId = warehouseId
|
||||
const locations = await overseasGet(context, '查询公共库位列表', query(`/api/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/locations`, { page: 1, size: 20 }))
|
||||
assertPage(context, '公共库位列表', locations)
|
||||
const location = records(locations.data)[0]
|
||||
if (location) {
|
||||
context.assert(MODULE, '公共库位关键字段正确', locations.response,
|
||||
longId(location.id) && id(location.warehouseId) === warehouseId && id(location.customerId) === '0'
|
||||
&& nonBlank(location.locationCode) && nonBlank(location.status),
|
||||
`id=${id(location.id) || 'missing'}, customerId=${id(location.customerId) || 'missing'}, status=${location.status || 'missing'}; ${trace(locations.response)}`, OPTIONS)
|
||||
const detail = await overseasGet(context, '查询公共库位详情', `/api/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/locations/${encodeURIComponent(id(location.id))}`)
|
||||
context.assert(MODULE, '公共库位详情对应列表记录', detail.response,
|
||||
id(detail.data?.id) === id(location.id) && id(detail.data?.warehouseId) === warehouseId && id(detail.data?.customerId) === '0',
|
||||
`listId=${id(location.id)}, detailId=${id(detail.data?.id) || 'missing'}; ${trace(detail.response)}`, OPTIONS)
|
||||
}
|
||||
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]
|
||||
@@ -167,13 +178,123 @@ export async function run(context) {
|
||||
}
|
||||
|
||||
try {
|
||||
await verifyLocationLifecycle(context, warehouseId)
|
||||
await verifyPalletLifecycle(context, warehouseId)
|
||||
context.context.phase4OverseasWarehouse = { customerId, warehouseId, palletId: context.createdResources.phase4OverseasWarehouse.palletId }
|
||||
await verifyRelabelLifecycle(context, customerId, warehouseId)
|
||||
context.context.phase4OverseasWarehouse = {
|
||||
customerId, warehouseId,
|
||||
palletId: context.createdResources.phase4OverseasWarehouse.palletId,
|
||||
relabelNo: context.createdResources.phase4OverseasWarehouse.relabelNo,
|
||||
}
|
||||
} finally {
|
||||
if (ownedWarehouse) await retireWarehouseFixture(context, customerId, warehouseId)
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyLocationLifecycle(context, warehouseId) {
|
||||
const marker = String(context.marker || '').replace(/[^A-Za-z0-9_-]/g, '').slice(-40).toUpperCase() || 'PHASE4'
|
||||
const locationCode = `ACC-${marker}`.slice(0, 64)
|
||||
let locationId = ''
|
||||
try {
|
||||
const created = await overseasMutation(context, 'POST', '创建本轮公共库位',
|
||||
`/api/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/locations`, {
|
||||
zoneCode: 'ACC', locationCode, locationType: 'STORAGE', maxLoadKg: 1000,
|
||||
status: 'ACTIVE', remark: context.marker,
|
||||
})
|
||||
locationId = id(created.data)
|
||||
context.createdResources.phase4OverseasWarehouse.locationId = locationId || null
|
||||
context.assert(MODULE, '公共库位创建返回长ID', created.response, longId(locationId),
|
||||
`locationId=${locationId || 'missing'}, marker=${context.marker}; ${trace(created.response)}`, OPTIONS)
|
||||
if (!locationId) return
|
||||
const detailPath = `/api/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/locations/${encodeURIComponent(locationId)}`
|
||||
const detail = await overseasGet(context, '查询本轮公共库位详情', detailPath)
|
||||
context.assert(MODULE, '本轮公共库位创建事实正确', detail.response,
|
||||
id(detail.data?.id) === locationId && id(detail.data?.customerId) === '0'
|
||||
&& detail.data?.locationCode === locationCode && detail.data?.remark === context.marker,
|
||||
`locationId=${id(detail.data?.id) || 'missing'}, code=${detail.data?.locationCode || 'missing'}; ${trace(detail.response)}`, OPTIONS)
|
||||
const updated = await overseasMutation(context, 'PUT', '编辑本轮公共库位', detailPath, {
|
||||
zoneCode: 'ACC', locationCode, locationType: 'STORAGE', maxLoadKg: 1200,
|
||||
status: 'ACTIVE', remark: `${context.marker}-edited`,
|
||||
})
|
||||
context.assert(MODULE, '公共库位编辑成功', updated.response, updated.data === null,
|
||||
`locationId=${locationId}; ${trace(updated.response)}`, OPTIONS)
|
||||
const after = await overseasGet(context, '回查公共库位编辑结果', detailPath)
|
||||
context.assert(MODULE, '公共库位编辑字段已生效', after.response,
|
||||
Number(after.data?.maxLoadKg) === 1200 && after.data?.remark === `${context.marker}-edited`,
|
||||
`maxLoadKg=${after.data?.maxLoadKg ?? 'missing'}, remark=${after.data?.remark || 'missing'}; ${trace(after.response)}`, OPTIONS)
|
||||
} finally {
|
||||
if (locationId) {
|
||||
const removed = await overseasMutation(context, 'DELETE', '删除本轮公共库位',
|
||||
`/api/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/locations/${encodeURIComponent(locationId)}`)
|
||||
context.createdResources.phase4OverseasWarehouse.locationTerminalStatus = removed.data === null ? 'DELETED' : null
|
||||
context.assert(MODULE, '本轮公共库位已删除收口', removed.response, removed.data === null,
|
||||
`locationId=${locationId}, terminalStatus=${context.createdResources.phase4OverseasWarehouse.locationTerminalStatus || 'missing'}; ${trace(removed.response)}`, OPTIONS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyRelabelLifecycle(context, customerId, warehouseId) {
|
||||
const marker = String(context.marker || 'PHASE4')
|
||||
const idempotencyKey = `${marker}:RELABEL`.slice(0, 255)
|
||||
const skuSuffix = marker.replace(/[^A-Za-z0-9]/g, '').slice(-20).toUpperCase() || 'PHASE4'
|
||||
const payload = {
|
||||
customerOrderNo: `${marker}-RELABEL`, overseasWarehouseId: warehouseId,
|
||||
labelDeadline: new Date(Date.now() + 86400000).toISOString().slice(0, 19),
|
||||
contactName: 'Phase4 QA', contactPhone: '+1-213-555-0199', remark: marker, idempotencyKey,
|
||||
items: [{ skuCode: `OLD-${skuSuffix}`.slice(0, 128), productNameZh: `${marker} 换标商品`,
|
||||
totalPackageCount: 2, notifiedPackageCount: 2, shipmentNo: `SHIP-${skuSuffix}`.slice(0, 128),
|
||||
newSkuCode: `NEW-${skuSuffix}`.slice(0, 128), labelRequirement: marker }],
|
||||
}
|
||||
let relabelNo = ''
|
||||
let terminalStatus = ''
|
||||
try {
|
||||
const created = await overseasMutation(context, 'POST', '创建本轮换标单',
|
||||
query('/api/platform/v1/last-leg/relabel-applications', { customerId }), payload)
|
||||
relabelNo = id(created.data?.application?.relabelNo)
|
||||
context.createdResources.phase4OverseasWarehouse.relabelNo = relabelNo || null
|
||||
context.assert(MODULE, '换标单创建事实正确', created.response,
|
||||
nonBlank(relabelNo) && created.data?.application?.customerOrderNo === payload.customerOrderNo
|
||||
&& created.data?.application?.status === 'PENDING_PROCESSING'
|
||||
&& Array.isArray(created.data?.items) && created.data.items.length === 1,
|
||||
`relabelNo=${relabelNo || 'missing'}, status=${created.data?.application?.status || 'missing'}; ${trace(created.response)}`, OPTIONS)
|
||||
if (!relabelNo) return
|
||||
|
||||
const replay = await overseasMutation(context, 'POST', '换标单幂等精确重放',
|
||||
query('/api/platform/v1/last-leg/relabel-applications', { customerId }), payload)
|
||||
context.assert(MODULE, '换标单精确重放返回同一单号', replay.response,
|
||||
replay.data?.application?.relabelNo === relabelNo,
|
||||
`expected=${relabelNo}, actual=${replay.data?.application?.relabelNo || 'missing'}; ${trace(replay.response)}`, OPTIONS)
|
||||
|
||||
const conflict = await context.requestOverseas('POST',
|
||||
query('/api/platform/v1/last-leg/relabel-applications', { customerId }), { ...payload, remark: `${marker}-DIFFERENT` })
|
||||
context.addStep(MODULE, '换标单同幂等键不同请求返回A0409', conflict, businessConflict(conflict), trace(conflict), OPTIONS)
|
||||
|
||||
const processing = await overseasMutation(context, 'PUT', '换标单进入处理中',
|
||||
`/api/platform/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/status`,
|
||||
{ status: 'PROCESSING', remark: marker })
|
||||
context.assert(MODULE, '换标单状态推进为PROCESSING', processing.response,
|
||||
processing.data?.application?.status === 'PROCESSING',
|
||||
`status=${processing.data?.application?.status || 'missing'}; ${trace(processing.response)}`, OPTIONS)
|
||||
const completed = await overseasMutation(context, 'PUT', '完成本轮换标单',
|
||||
`/api/platform/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/status`,
|
||||
{ status: 'COMPLETED', remark: `${marker}-completed` })
|
||||
terminalStatus = completed.data?.application?.status || ''
|
||||
context.createdResources.phase4OverseasWarehouse.relabelTerminalStatus = terminalStatus || null
|
||||
context.assert(MODULE, '本轮换标单已完成收口', completed.response, terminalStatus === 'COMPLETED',
|
||||
`relabelNo=${relabelNo}, status=${terminalStatus || 'missing'}; ${trace(completed.response)}`, OPTIONS)
|
||||
} finally {
|
||||
if (relabelNo && terminalStatus !== 'COMPLETED') {
|
||||
const cancelled = await context.requestOverseas('PUT',
|
||||
`/api/platform/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/status`,
|
||||
{ status: 'CANCELLED', remark: `${marker}-acceptance-cleanup` })
|
||||
const status = cancelled?.body?.data?.application?.status
|
||||
context.createdResources.phase4OverseasWarehouse.relabelTerminalStatus = status || null
|
||||
context.addStep(MODULE, '异常路径换标单取消收口', cancelled,
|
||||
cancelled?.http === 200 && String(cancelled?.body?.code) === '200' && status === 'CANCELLED', trace(cancelled), OPTIONS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function createWarehouseFixture(context, customerId) {
|
||||
const suffix = String(context.marker || '').replace(/[^A-Za-z0-9]/g, '').slice(-20).toUpperCase()
|
||||
const payload = {
|
||||
|
||||
Reference in New Issue
Block a user