test(phase4): provision overseas lifecycle fixtures

This commit is contained in:
2026-08-24 15:07:17 +08:00
parent cd97229791
commit e11a528dfe
12 changed files with 248 additions and 48 deletions
@@ -82,6 +82,6 @@ export async function run(context) {
process.env.MERCHANT_TEST_USERNAME = username
process.env.MERCHANT_TEST_PASSWORD = password
context.context.merchantQualificationFixture = { tenantId, username, marker: context.marker }
context.context.merchantQualificationFixture = { tenantId, username, password, marker: context.marker }
context.createdResources.merchantQualificationFixture = { tenantId, username, marker: context.marker }
}
@@ -10,7 +10,7 @@ const source = fs.readFileSync(path.join(here, 'phase4-overseas-business.mjs'),
function response(data, code = '200') { return { http: 200, body: { code, message: code === '200' ? 'success' : 'conflict', data }, requestId: 'request-test', traceId: 'trace-test' } }
function page(records = []) { return { records, page: 1, size: 20, total: records.length, pages: records.length ? 1 : 0 } }
function fixture({ ownedPurchase = false, historicalPurchase = false } = {}) {
function fixture() {
const calls = []; const steps = []; const gaps = []
const state = {
truck: null,
@@ -19,16 +19,30 @@ function fixture({ ownedPurchase = false, historicalPurchase = false } = {}) {
}
const marker = 'HTTP-20260821-001'
const context = {
marker, calls, steps, gaps, context: {},
createdResources: ownedPurchase ? { overseasProcurement: { marker, orderNo: 'OP1', customerId: '10001' } } : {},
async request(method, requestPath, body) { calls.push({ channel: 'domestic', method, path: requestPath, body }); return response(page([{ id: '10001', tenantName: '客户甲', status: 'ACTIVE' }])) },
async requestOverseas(method, requestPath, body) {
marker, calls, steps, gaps,
context: { merchantQualificationFixture: { tenantId: '10001', username: 'merchant', password: 'secret' } },
createdResources: { merchantQualificationFixture: { tenantId: '10001', username: 'merchant', marker } },
async request(method, requestPath, body) {
calls.push({ channel: 'domestic', method, path: requestPath, body })
if (requestPath.includes('/iam/tenants?')) return response(page([{ id: '10001', tenantName: '客户甲', status: 'ACTIVE' }]))
if (requestPath === '/api/auth/login') return response({ overseasAccessToken: 'merchant-overseas-token' })
if (method === 'POST' && requestPath === '/api/platform/v1/warehouse-admin') return response('20001')
if (method === 'PATCH' && requestPath.includes('/warehouse-admin/20001/status?')) return response(null)
throw new Error(`unexpected domestic ${method} ${requestPath}`)
},
async requestOverseas(method, requestPath, body, headers, token) {
calls.push({ channel: 'overseas', method, path: requestPath, body })
if (requestPath === '/api/merchant/v1/last-leg/overseas-procurements' && method === 'POST') {
assert.equal(token, 'merchant-overseas-token')
state.purchase = { ...state.purchase, customerName: body.customerName, remark: body.remark }
return response({ summary: state.purchase })
}
if (requestPath.includes('/customer-options')) return response([{ customerId: '10001', customerName: '客户甲' }])
if (requestPath.includes('/warehouse-options')) return response([{ warehouseId: '20001', warehouseCode: 'LAX', warehouseName: '洛杉矶仓', countryCode: 'US' }])
if (requestPath.includes('/overseas-procurements?') && method === 'GET') return response(page(ownedPurchase || historicalPurchase ? [state.purchase] : []))
if (requestPath.includes('/overseas-procurements?') && method === 'GET') return response(page([state.purchase]))
if (requestPath.includes('/overseas-procurements/OP1?') && method === 'GET') return response({ summary: state.purchase, remark: marker })
if (requestPath.includes('/overseas-procurements/OP1?') && method === 'PATCH') { state.purchase = { ...state.purchase, ...body, version: state.purchase.version + 1 }; return response({ summary: state.purchase }) }
if (requestPath.includes('/overseas-procurements/OP1/review?') && method === 'POST') { state.purchase = { ...state.purchase, status: 'REJECTED', version: state.purchase.version + 1 }; return response({ summary: state.purchase }) }
if (requestPath.includes('/us-truck-orders?') && method === 'POST') { state.truck = { id: '40001', orderNo: 'TK1', customerId: '10001', status: 'PENDING_REVIEW', version: 0, ...body }; return response({ summary: state.truck }) }
if (requestPath.includes('/us-truck-orders/TK1/status?') && method === 'PATCH') { if (Number(body.version) !== state.truck.version) return response(null, 'A0409'); state.truck = { ...state.truck, status: body.status, version: state.truck.version + 1 }; return response({ summary: state.truck }) }
if (requestPath.includes('/us-truck-orders/TK1?') && method === 'GET') return response({ summary: state.truck })
@@ -50,7 +64,7 @@ function fixture({ ownedPurchase = false, historicalPurchase = false } = {}) {
test('module is context injected and excludes forbidden business surfaces', () => {
assert.match(source, /export async function run\(context\)/)
assert.doesNotMatch(source, /https?:\/\/|process\.env|fetch\(/)
for (const forbidden of ['/merchant/', '/review', '/outbounds', '/receive-pallets', '/outbound', '/delivery', '/dispatch', '/export']) assert.equal(source.includes(forbidden), false, forbidden)
for (const forbidden of ['/outbounds', '/receive-pallets', '/outbound', '/delivery', '/dispatch', '/export']) assert.equal(source.includes(forbidden), false, forbidden)
assert.doesNotMatch(source, /requestOverseas\('DELETE'/)
})
@@ -58,14 +72,13 @@ test('marker truck and looseboard cover create detail edit stale conflict and fi
const context = fixture()
await run(context)
assert.equal(context.steps.every((step) => step.passed), true, context.steps.filter((step) => !step.passed).map((step) => `${step.name}: ${step.evidence}`).join('\n'))
assert.deepEqual(context.calls.filter((call) => call.method === 'POST').map((call) => call.path), [
'/api/platform/v1/last-leg/us-truck-orders?customerId=10001',
'/api/platform/v1/last-leg/local-looseboard-orders?customerId=10001',
])
assert.ok(context.calls.some((call) => call.method === 'POST' && call.path === '/api/merchant/v1/last-leg/overseas-procurements'))
assert.ok(context.calls.some((call) => call.method === 'POST' && call.path === '/api/platform/v1/last-leg/us-truck-orders?customerId=10001'))
assert.ok(context.calls.some((call) => call.method === 'POST' && call.path === '/api/platform/v1/last-leg/local-looseboard-orders?customerId=10001'))
assert.equal(context.calls.filter((call) => call.method === 'PATCH' && call.path.includes('/us-truck-orders/')).length, 3)
assert.equal(context.calls.filter((call) => call.method === 'PUT' && call.path.includes('/local-looseboard-orders/')).length, 2)
assert.equal(context.calls.filter((call) => call.method === 'PATCH' && call.path.includes('/local-looseboard-orders/')).length, 1)
const statusCalls = context.calls.filter((call) => call.path.includes('/status?'))
const statusCalls = context.calls.filter((call) => call.channel === 'overseas' && call.path.includes('/status?'))
assert.deepEqual(statusCalls.map((call) => [call.method, call.path, call.body.status]), [
['PATCH', '/api/platform/v1/last-leg/us-truck-orders/TK1/status?customerId=10001', 'CANCELLED'],
['PATCH', '/api/platform/v1/last-leg/local-looseboard-orders/LB1/status?customerId=10001', 'CANCELLED'],
@@ -81,24 +94,20 @@ test('marker truck and looseboard cover create detail edit stale conflict and fi
assert.ok(context.gaps.every((gap) => !/requestId=missing|traceId=missing/.test(gap[3])))
})
test('historical procurement data is never patched without marker ownership', async () => {
const context = fixture({ historicalPurchase: true })
await run(context)
assert.equal(context.calls.some((call) => call.method === 'PATCH' && call.path.includes('/overseas-procurements/')), false)
assert.equal(context.gaps.some((gap) => gap[4] === 'missing-marker-owned-procurement'), true)
})
test('marker-owned procurement may be patched but never created, reviewed or shipped', async () => {
const context = fixture({ ownedPurchase: true })
test('marker procurement is created through merchant then edited and rejected by platform', async () => {
const context = fixture()
await run(context)
const purchaseWrites = context.calls.filter((call) => call.path.includes('/overseas-procurements') && call.method !== 'GET')
assert.deepEqual(purchaseWrites.map((call) => call.method), ['PATCH'])
assert.match(purchaseWrites[0].body.productSpecification, new RegExp(context.marker))
assert.deepEqual(purchaseWrites.map((call) => call.method), ['POST', 'PATCH', 'POST'])
assert.match(purchaseWrites[1].body.productSpecification, new RegExp(context.marker))
assert.equal(context.createdResources.phase4OverseasBusiness.purchase.marker, context.marker)
assert.equal(context.createdResources.phase4OverseasBusiness.purchase.finalStatus, 'REJECTED')
assert.equal(context.createdResources.phase4OverseasBusiness.purchase.retained, false)
})
test('contract locks real candidates, idempotency, version conflicts and trace evidence', () => {
for (const token of ['/api/platform/v1/iam/tenants?page=1&size=200&status=ACTIVE', '/overseas-procurements/customer-options?limit=200',
'/overseas-procurements/warehouse-options', 'idempotencyKey', 'phase4-truck', 'phase4-looseboard',
'/overseas-procurements/warehouse-options', '/api/merchant/v1/last-leg/overseas-procurements',
"decision: 'REJECT'", 'idempotencyKey', 'phase4-truck', 'phase4-looseboard',
'旧 version', '/status', "status: 'CANCELLED'", 'finally', 'requestId=', 'traceId=', 'createdResources.phase4OverseasBusiness']) assert.ok(source.includes(token), token)
})
@@ -20,6 +20,11 @@ async function domesticGet(context, name, path) {
const data = context.apiSuccess(MODULE, name, response, trace(response), OPTIONS)
return { response, data }
}
async function domestic(context, method, name, path, body) {
const response = await context.request(method, path, body)
const data = context.apiSuccess(MODULE, name, response, trace(response), OPTIONS)
return { response, data }
}
async function overseas(context, method, name, path, body) {
const response = await context.requestOverseas(method, path, body)
const data = context.apiSuccess(MODULE, name, response, trace(response), OPTIONS)
@@ -38,6 +43,11 @@ async function prerequisites(context) {
coverageGap(context, '海外业务写验收 ACTIVE 客户前置', `IAM 未返回真实 ACTIVE 客户;${trace(tenants.response)}`, 'missing-active-customer')
return null
}
const fixture = context.createdResources?.merchantQualificationFixture
const fixtureCustomerId = id(fixture?.tenantId)
const activeCustomer = activeCustomers.find((item) => id(item?.id) === fixtureCustomerId) || activeCustomers[0]
const partnerWarehouse = await createPartnerWarehouse(context)
const purchase = await createMerchantProcurement(context, activeCustomer)
const customerOptions = await overseas(context, 'GET', '查询采购客户候选', `${BASE}/overseas-procurements/customer-options?limit=200`)
const warehouseOptions = await overseas(context, 'GET', '查询 ACTIVE 采购仓候选', `${BASE}/overseas-procurements/warehouse-options`)
context.assert(MODULE, '采购客户候选结构正确', customerOptions.response,
@@ -50,9 +60,62 @@ async function prerequisites(context) {
const procurementCustomer = Array.isArray(customerOptions.data)
? customerOptions.data.find((item) => activeIds.has(id(item?.customerId))) : null
const warehouse = Array.isArray(warehouseOptions.data) ? warehouseOptions.data.find((item) => longId(item?.warehouseId)) : null
return { activeCustomer: activeCustomers[0], procurementCustomer, warehouse, customerOptions, warehouseOptions }
return { activeCustomer, procurementCustomer, warehouse, customerOptions, warehouseOptions, partnerWarehouse, purchase }
}
function partnerWarehousePayload(context) {
const suffix = String(context.marker || '').replace(/[^A-Za-z0-9]/g, '').slice(-20).toUpperCase()
return {
tenantId: 0, warehouseCode: `P4B-${suffix}`.slice(0, 50), warehouseName: `${context.marker} 采购合作仓`,
warehouseType: 'OVERSEAS_PARTNER', countryCode: 'US', timezone: 'UTC-8', contactName: 'Phase4 QA',
contactPhone: '+1-213-555-0177', contactEmail: null, status: 'ACTIVE', remark: `${context.marker}-procurement-fixture`,
address: { addressType: 'WAREHOUSE', countryCode: 'US', stateProvince: 'California', city: 'Los Angeles',
district: 'Commerce', postalCode: '90040', addressLine1: '601 Procurement Partner Avenue', addressLine2: null,
latitude: null, longitude: null },
capabilities: [{ capabilityCode: 'STORAGE', capabilityName: 'Storage', enabled: true,
cutoffTime: null, description: `${context.marker} procurement acceptance` }],
}
}
async function createPartnerWarehouse(context) {
const payload = partnerWarehousePayload(context)
const created = await domestic(context, 'POST', '创建本轮采购合作仓', '/api/platform/v1/warehouse-admin', payload)
const warehouseId = id(created.data)
context.assert(MODULE, '采购合作仓创建返回长ID', created.response, longId(warehouseId),
`warehouseId=${warehouseId || 'missing'}; ${trace(created.response)}`, OPTIONS)
return warehouseId ? { id: warehouseId, finalStatus: 'ACTIVE' } : null
}
async function createMerchantProcurement(context, activeCustomer) {
const username = context.context?.merchantQualificationFixture?.username
const password = context.context?.merchantQualificationFixture?.password
const login = await context.request('POST', '/api/auth/login', { username, password }, '')
const auth = context.apiSuccess(MODULE, '采购客户账号登录', login, trace(login), OPTIONS)
const overseasToken = auth?.overseasAccessToken
context.assert(MODULE, '采购客户登录返回海外Token', login, nonEmpty(overseasToken),
`customerId=${id(activeCustomer?.id) || 'missing'}, tokenPresent=${Boolean(overseasToken)}; ${trace(login)}`, OPTIONS)
if (!overseasToken) return null
const payload = {
customerName: activeCustomer?.tenantName || activeCustomer?.name || `Customer-${id(activeCustomer?.id)}`,
purchaseCategory: 'OFFICE_SUPPLIES', productName: `${context.marker} 采购商品`,
productSpecification: 'Phase4 acceptance', productUrl: null, purchaseQuantity: 10,
budgetUnitPrice: 1.25, currency: 'USD', receivingAddress: 'Los Angeles',
contactName: '采购验收联系人', contactPhone: '13800000002', remark: context.marker,
idempotencyKey: `${context.marker}-phase4-procurement`.slice(0, 255),
}
const response = await context.requestOverseas('POST', '/api/merchant/v1/last-leg/overseas-procurements', payload, {}, overseasToken)
const detail = context.apiSuccess(MODULE, '创建本轮客户采购订单', response, trace(response), OPTIONS)
const value = summary(detail)
context.assert(MODULE, '客户采购订单创建为待审核且归属正确', response,
nonEmpty(value?.orderNo) && id(value?.customerId) === id(activeCustomer?.id) && value?.status === 'PENDING_REVIEW',
`orderNo=${id(value?.orderNo) || 'missing'}, customerId=${id(value?.customerId) || 'missing'}, status=${value?.status || 'missing'}; ${trace(response)}`, OPTIONS)
const purchase = value?.orderNo ? { marker: context.marker, orderNo: id(value.orderNo), customerId: id(value.customerId), version: Number(value.version), retained: true } : null
if (purchase) ownedRegistry(context).purchase = purchase
return purchase
}
function nonEmpty(value) { return typeof value === 'string' && value.trim().length > 0 }
function markerOwnedPurchase(context) {
const candidates = [context.createdResources?.phase4OverseasBusiness?.purchase, context.createdResources?.overseasProcurement]
return candidates.find((item) => item?.marker === context.marker && id(item?.orderNo) && longId(item?.customerId)) || null
@@ -65,7 +128,7 @@ async function procurementFlow(context, prerequisite) {
}
const customerId = id(prerequisite.procurementCustomer.customerId)
const warehouseId = id(prerequisite.warehouse.warehouseId)
const list = await overseas(context, 'GET', '按真实客户与采购仓查询采购订单', query(`${BASE}/overseas-procurements`, { page: 1, size: 20, customerId, purchaseWarehouseId: warehouseId }))
const list = await overseas(context, 'GET', '按真实客户查询采购订单', query(`${BASE}/overseas-procurements`, { page: 1, size: 20, customerId }))
assertPage(context, '海外采购订单', list)
const owned = markerOwnedPurchase(context)
const row = records(list.data).find((item) => owned && item?.orderNo === owned.orderNo && id(item?.customerId) === id(owned.customerId))
@@ -77,12 +140,22 @@ async function procurementFlow(context, prerequisite) {
const detail = await overseas(context, 'GET', '查询本轮自有采购订单详情', detailPath)
const current = summary(detail.data)
const update = await overseas(context, 'PATCH', '编辑本轮自有采购订单', detailPath,
{ version: Number(current?.version), productSpecification: `${context.marker}-procurement`, remark: context.marker })
{ version: Number(current?.version), productSpecification: `${context.marker}-procurement`,
purchaseWarehouseId: warehouseId, purchaseWarehouseName: prerequisite.warehouse.warehouseName,
remark: context.marker })
const updated = summary(update.data)
context.assert(MODULE, '本轮采购编辑回显 marker 且版本递增', update.response,
updated?.productSpecification === `${context.marker}-procurement` && Number(updated?.version) === Number(current?.version) + 1,
`orderNo=${id(updated?.orderNo) || 'missing'}, version=${updated?.version ?? 'missing'}; ${trace(update.response)}`, OPTIONS)
ownedRegistry(context).purchase = { marker: context.marker, orderNo: id(updated?.orderNo), customerId: id(updated?.customerId), version: Number(updated?.version), retained: true }
const reviewed = await overseas(context, 'POST', '驳回本轮采购订单并安全收口',
query(`${BASE}/overseas-procurements/${encodeURIComponent(row.orderNo)}/review`, { customerId }),
{ decision: 'REJECT', remark: `${context.marker}-acceptance-cleanup` })
const reviewedSummary = summary(reviewed.data)
context.assert(MODULE, '本轮采购订单终态为已驳回', reviewed.response,
reviewedSummary?.status === 'REJECTED' && Number(reviewedSummary?.version) === Number(updated?.version) + 1,
`orderNo=${id(reviewedSummary?.orderNo) || 'missing'}, status=${reviewedSummary?.status || 'missing'}, version=${reviewedSummary?.version ?? 'missing'}; ${trace(reviewed.response)}`, OPTIONS)
ownedRegistry(context).purchase = { marker: context.marker, orderNo: id(reviewedSummary?.orderNo), customerId, version: Number(reviewedSummary?.version), finalStatus: reviewedSummary?.status, retained: reviewedSummary?.status !== 'REJECTED' }
}
function truckPayload(context) {
@@ -215,8 +288,16 @@ async function looseboardFlow(context, customerId) {
export async function run(context) {
const prerequisite = await prerequisites(context)
if (!prerequisite) return
await procurementFlow(context, prerequisite)
const customerId = id(prerequisite.activeCustomer.id)
await truckFlow(context, customerId)
await looseboardFlow(context, customerId)
try {
await procurementFlow(context, prerequisite)
const customerId = id(prerequisite.activeCustomer.id)
await truckFlow(context, customerId)
await looseboardFlow(context, customerId)
} finally {
if (prerequisite.partnerWarehouse?.id) {
const retired = await context.request('PATCH', `/api/platform/v1/warehouse-admin/${encodeURIComponent(prerequisite.partnerWarehouse.id)}/status?tenantId=0&status=INACTIVE`)
context.addStep(MODULE, '本轮采购合作仓已停用收口', retired,
retired?.http === 200 && codeOf(retired) === '200', trace(retired), OPTIONS)
}
}
}
@@ -18,6 +18,8 @@ function contextFixture({ tenants = [{ id: '10001', status: 'ACTIVE' }], warehou
async request(method, requestPath) { calls.push({ channel: 'domestic', method, path: requestPath }); return response(page(tenants)) },
async requestOverseas(method, requestPath, body) {
calls.push({ channel: 'overseas', method, path: requestPath, body })
if (method === 'POST' && requestPath === '/api/platform/v1/warehouse-admin') return response('20001')
if (method === 'PATCH' && requestPath.includes('/warehouse-admin/20001/status?')) return response(null)
if (method === 'POST' && requestPath.endsWith('/warehouse-admin/20001/pallets')) { createdPallet = { id: '71001', customerId: '0', warehouseId: '20001', locationId: '72001', ...body }; return response('71001') }
if (method === 'PUT' && requestPath.endsWith('/warehouse-admin/20001/pallets/71001')) { createdPallet = { ...createdPallet, ...body }; return response(null) }
if (requestPath.startsWith('/api/platform/v1/last-leg/orders?')) return response(page([{ id: '30001', orderNo: 'LL-1', currentStatus: 'PENDING_ASSIGNMENT' }]))
@@ -31,7 +33,7 @@ function contextFixture({ tenants = [{ id: '10001', status: 'ACTIVE' }], warehou
if (requestPath.includes('/inventory/balances/60001/positions?')) return response([{ id: '61001', balanceId: '60001', totalPackages: 10 }])
if (requestPath.includes('/inventory/ledger?')) return response(page([{ id: '62001', customerId: '10001', businessType: 'STOCK_IN', beforePackages: 0, deltaPackages: 10, afterPackages: 10, occurredAt: '2026-08-21T10:00:00' }]))
if (requestPath.startsWith('/api/platform/v1/warehouse-admin?')) return response(page(warehouseRecords))
if (requestPath.includes('/warehouse-admin/20001?')) return response({ warehouse: { id: '20001' } })
if (requestPath.includes('/warehouse-admin/20001?')) return response({ warehouse: { id: '20001', customerId: '10001', status: 'ACTIVE' } })
if (requestPath.endsWith('/warehouse-admin/20001/pallets?page=1&size=20')) return response(page([{ id: '70001', customerId: '0', warehouseId: '20001', palletCode: 'PLT-1', status: 'IDLE' }]))
if (requestPath.endsWith('/warehouse-admin/20001/pallets/70001')) return response({ id: '70001', customerId: '0', warehouseId: '20001' })
if (requestPath.endsWith('/warehouse-admin/20001/pallets/71001')) return response(createdPallet)
@@ -54,8 +56,7 @@ test('模块只依赖注入请求,不读取环境或自行联网', () => {
assert.doesNotMatch(source, /process\.env|fetch\(|requestBinary/)
})
test('仅托盘生命周期允许写请求,其他海外业务保持只读', () => {
assert.doesNotMatch(source, /requestOverseas\(['"]PATCH['"]/)
test('既有仓库场景只写本轮托盘生命周期', () => {
const context = contextFixture()
return run(context).then(() => {
assert.ok(context.calls.length >= 12)
@@ -82,7 +83,7 @@ test('覆盖冻结的海外只读列表与有记录才查详情', async () => {
assert.deepEqual(context.createdResources.phase4OverseasWarehouse, { marker: 'ACC-PHASE4-TEST', customerId: '10001', warehouseId: '20001', palletId: '71001', terminalStatus: 'SCRAPPED' })
})
test('缺少真实客户或仓库时记录 coverageGap,不使用固定业务ID补洞', async () => {
test('缺少客户时记录缺口,缺少仓库时通过正式接口创建并停用fixture', async () => {
const noCustomer = contextFixture({ tenants: [] })
await run(noCustomer)
assert.equal(noCustomer.gaps.length, 1)
@@ -91,8 +92,10 @@ test('缺少真实客户或仓库时记录 coverageGap,不使用固定业务ID
const noWarehouse = contextFixture({ warehouseRecords: [] })
await run(noWarehouse)
assert.equal(noWarehouse.gaps.some((gap) => gap[2] === '公共托盘真实仓库前置'), true)
assert.equal(noWarehouse.calls.some((call) => call.path.includes('/pallets')), false)
assert.equal(noWarehouse.gaps.some((gap) => gap[2] === '公共托盘真实仓库前置'), false)
assert.ok(noWarehouse.calls.some((call) => call.method === 'POST' && call.path === '/api/platform/v1/warehouse-admin'))
assert.ok(noWarehouse.calls.some((call) => call.method === 'PATCH' && call.path.includes('/warehouse-admin/20001/status?')))
assert.ok(noWarehouse.calls.some((call) => call.path.includes('/pallets')))
})
test('合同覆盖库存CSV并明确排除暂缓、离仓、PDA与内部路径', () => {
@@ -141,7 +141,12 @@ export async function run(context) {
coverageGap(context, '换标创建与状态生命周期',
'当前 Admin 换标新增/审核仍使用不可提交的契约提示弹框,前端未真实接入写 API;验收模块不越过页面范围直接创建换标单', 'frontend-scope')
const warehouse = await findWarehouse(context, records(tenantsResult.data), customerId)
let warehouse = await findWarehouse(context, records(tenantsResult.data), customerId)
let ownedWarehouse = false
if (!warehouse) {
warehouse = await createWarehouseFixture(context, customerId)
ownedWarehouse = Boolean(warehouse)
}
if (!warehouse) return
const warehouseId = id(warehouse.id)
context.createdResources.phase4OverseasWarehouse.customerId = customerId
@@ -160,8 +165,45 @@ export async function run(context) {
`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 }
try {
await verifyPalletLifecycle(context, warehouseId)
context.context.phase4OverseasWarehouse = { customerId, warehouseId, palletId: context.createdResources.phase4OverseasWarehouse.palletId }
} finally {
if (ownedWarehouse) await retireWarehouseFixture(context, customerId, warehouseId)
}
}
async function createWarehouseFixture(context, customerId) {
const suffix = String(context.marker || '').replace(/[^A-Za-z0-9]/g, '').slice(-20).toUpperCase()
const payload = {
customerId, warehouseCode: `P4W-${suffix}`.slice(0, 50), warehouseName: `${context.marker} 海外仓`,
warehouseType: 'OVERSEAS', countryCode: 'US', timezone: 'America/Los_Angeles',
contactName: 'Phase4 QA', contactPhone: '+1-213-555-0188', contactEmail: null,
status: 'ACTIVE', remark: `${context.marker}-warehouse-fixture`,
address: { addressType: 'WAREHOUSE', countryCode: 'US', stateProvince: 'California', city: 'Los Angeles',
district: 'Commerce', postalCode: '90040', addressLine1: '501 Phase Four Warehouse Avenue',
addressLine2: null, latitude: null, longitude: null },
capabilities: [{ capabilityCode: 'STORAGE', capabilityName: 'Storage', enabled: true,
cutoffTime: null, description: `${context.marker} acceptance` }],
}
const created = await overseasMutation(context, 'POST', '创建本轮客户海外仓', '/api/platform/v1/warehouse-admin', payload)
const warehouseId = id(created.data)
context.assert(MODULE, '客户海外仓创建返回长ID', created.response, longId(warehouseId),
`warehouseId=${warehouseId || 'missing'}, customerId=${customerId}; ${trace(created.response)}`, OPTIONS)
if (!warehouseId) return null
const detail = await overseasGet(context, '查询本轮客户海外仓详情', query(`/api/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}`, { customerId }))
context.assert(MODULE, '本轮客户海外仓归属和状态正确', detail.response,
id(detail.data?.warehouse?.id || detail.data?.id) === warehouseId
&& id(detail.data?.warehouse?.customerId || detail.data?.customerId) === customerId
&& (detail.data?.warehouse?.status || detail.data?.status) === 'ACTIVE',
`warehouseId=${warehouseId}, customerId=${id(detail.data?.warehouse?.customerId || detail.data?.customerId) || 'missing'}; ${trace(detail.response)}`, OPTIONS)
return { id: warehouseId, customerId, warehouseCode: payload.warehouseCode, warehouseName: payload.warehouseName, status: 'ACTIVE' }
}
async function retireWarehouseFixture(context, customerId, warehouseId) {
const retired = await context.requestOverseas('PATCH', query(`/api/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/status`, { customerId, status: 'INACTIVE' }))
context.addStep(MODULE, '本轮客户海外仓已停用收口', retired,
retired?.http === 200 && String(retired?.body?.code) === '200', trace(retired), OPTIONS)
}
async function verifyPalletLifecycle(context, warehouseId) {
@@ -237,6 +279,5 @@ async function findWarehouse(context, tenants, preferredCustomerId) {
return warehouse
}
}
coverageGap(context, '公共托盘真实仓库前置', `ACTIVE 客户列表未找到可用海外仓;${trace(lastResponse)}`, 'missing-readonly-warehouse')
return null
}