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

223 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const MODULE = 'phase4-overseas-business'
const SCENARIO = 'phase4-overseas-business-write-lifecycle'
const OPTIONS = { scenario: SCENARIO, deploymentBlocker: true }
const BASE = '/api/platform/v1/last-leg'
function id(value) { return value == null ? '' : String(value) }
function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) }
function trace(response) { return `HTTP=${response?.http ?? 'missing'}, code=${codeOf(response)}, requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}` }
function records(page) { return Array.isArray(page?.records) ? page.records : [] }
function query(path, params) { const search = new URLSearchParams(); for (const [key, value] of Object.entries(params)) if (value !== '' && value != null) search.set(key, String(value)); return `${path}?${search}` }
function summary(data) { return data?.summary && typeof data.summary === 'object' ? data.summary : data }
function conflict(response) { return response?.http === 409 || (response?.http === 200 && codeOf(response).endsWith('409')) }
function longId(value) { return /^[1-9]\d*$/.test(id(value)) }
function pageValid(page) { return Array.isArray(page?.records) && Number(page?.page) >= 1 && Number(page?.size) >= 1 && Number(page?.total) >= page.records.length && Number(page?.pages) >= 0 }
function coverageGap(context, name, reason, category = 'prerequisite') { context.addCoverageGap(MODULE, SCENARIO, name, reason, category) }
function ownedRegistry(context) { context.createdResources.phase4OverseasBusiness ??= { marker: context.marker }; return context.createdResources.phase4OverseasBusiness }
async function domesticGet(context, name, path) {
const response = await context.request('GET', path)
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)
return { response, data }
}
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)
}
async function prerequisites(context) {
const tenants = await domesticGet(context, '查询 ACTIVE 客户候选', '/api/platform/v1/iam/tenants?page=1&size=200&status=ACTIVE')
assertPage(context, 'ACTIVE 客户候选', tenants)
const activeCustomers = records(tenants.data).filter((item) => longId(item?.id) && item?.status === 'ACTIVE')
if (!activeCustomers.length) {
coverageGap(context, '海外业务写验收 ACTIVE 客户前置', `IAM 未返回真实 ACTIVE 客户;${trace(tenants.response)}`, 'missing-active-customer')
return null
}
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,
Array.isArray(customerOptions.data) && customerOptions.data.every((item) => longId(item?.customerId) && String(item?.customerName || '').trim()),
`options=${customerOptions.data?.length ?? 'missing'}; ${trace(customerOptions.response)}`, OPTIONS)
context.assert(MODULE, '采购仓候选结构正确', warehouseOptions.response,
Array.isArray(warehouseOptions.data) && warehouseOptions.data.every((item) => longId(item?.warehouseId) && String(item?.warehouseName || '').trim()),
`options=${warehouseOptions.data?.length ?? 'missing'}; ${trace(warehouseOptions.response)}`, OPTIONS)
const activeIds = new Set(activeCustomers.map((item) => id(item.id)))
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 }
}
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
}
async function procurementFlow(context, prerequisite) {
if (!prerequisite.procurementCustomer || !prerequisite.warehouse) {
coverageGap(context, '采购真实候选与列表前置', `ACTIVE 客户与采购客户候选无交集或无 ACTIVE 采购仓;customerOptions=${prerequisite.customerOptions.data?.length ?? 0}, warehouseOptions=${prerequisite.warehouseOptions.data?.length ?? 0};customer=${trace(prerequisite.customerOptions.response)};warehouse=${trace(prerequisite.warehouseOptions.response)}`, 'missing-procurement-candidate')
return
}
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 }))
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))
if (!row) {
coverageGap(context, '采购 PATCH 本轮资源', `平台无采购 create,列表中没有 marker=${context.marker} 的本轮自有采购单;禁止修改历史客户数据;${trace(list.response)}`, 'missing-marker-owned-procurement')
return
}
const detailPath = query(`${BASE}/overseas-procurements/${encodeURIComponent(row.orderNo)}`, { customerId: id(row.customerId) })
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 })
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 }
}
function truckPayload(context) {
const suffix = context.marker.replace(/[^A-Za-z0-9]/g, '').slice(-16)
return {
transportType: 'PORT_PICKUP', origin: `验收起运-${suffix}`, originAddress: 'Long Beach',
destination: `验收目的-${suffix}`, destinationAddress: 'Ontario', containerType: '40HQ',
palletCount: 1, packageCount: 2, totalWeightKg: 100, totalVolumeCbm: 2.5, budgetAmount: 88,
currency: 'USD', contactName: '四期验收联系人', contactInfo: '13800000000',
requestedDate: new Date(Date.now() + 7 * 86400000).toISOString().slice(0, 10), remark: context.marker,
idempotencyKey: `${context.marker}-phase4-truck`.slice(0, 255),
}
}
async function cancelMarkerOrder(context, resource, base, cancellableStatus, label) {
if (!resource || resource.marker !== context.marker || !resource.orderNo || !longId(resource.customerId)) return
const detailPath = query(`${base}/${encodeURIComponent(resource.orderNo)}`, { customerId: resource.customerId })
const current = await overseas(context, 'GET', `清理前回查 marker ${label}订单`, detailPath)
const currentSummary = summary(current.data)
if (currentSummary?.status === 'CANCELLED') {
resource.finalStatus = 'CANCELLED'; resource.version = Number(currentSummary.version); resource.retained = false
return
}
if (currentSummary?.status !== cancellableStatus) {
resource.finalStatus = currentSummary?.status || resource.finalStatus; resource.version = Number(currentSummary?.version); resource.retained = true
coverageGap(context, `${label}测试数据最终处置`, `本轮 marker 资源 ${resource.orderNo} 当前状态 ${resource.finalStatus || 'missing'} 不允许安全取消;${trace(current.response)}`, 'unsafe-marker-terminal-state')
return
}
const statusPath = query(`${base}/${encodeURIComponent(resource.orderNo)}/status`, { customerId: resource.customerId })
const cancelled = await overseas(context, 'PATCH', `取消 marker ${label}订单`, statusPath,
{ version: Number(currentSummary.version), status: 'CANCELLED', remark: `${context.marker}-acceptance-cleanup` })
const cancelledSummary = summary(cancelled.data)
const verified = await overseas(context, 'GET', `回查 marker ${label}订单最终状态`, detailPath)
const finalSummary = summary(verified.data)
const passed = cancelledSummary?.status === 'CANCELLED' && finalSummary?.status === 'CANCELLED'
&& Number(finalSummary?.version) === Number(cancelledSummary?.version)
context.assert(MODULE, `${label}本轮测试数据最终取消`, verified.response, passed,
`orderNo=${resource.orderNo}, status=${finalSummary?.status || 'missing'}, version=${finalSummary?.version ?? 'missing'}; statusCall=${trace(cancelled.response)}; verify=${trace(verified.response)}`, OPTIONS)
resource.finalStatus = finalSummary?.status || cancelledSummary?.status || resource.finalStatus
resource.version = Number(finalSummary?.version ?? cancelledSummary?.version)
resource.retained = !passed
if (!passed) coverageGap(context, `${label}测试数据最终处置`, `本轮 marker 资源 ${resource.orderNo} 取消失败并保留;statusCall=${trace(cancelled.response)};verify=${trace(verified.response)}`, 'cleanup-failed-retained-marker-test-data')
}
async function truckFlow(context, customerId) {
const base = `${BASE}/us-truck-orders`
const payload = truckPayload(context)
const created = await overseas(context, 'POST', '创建 marker 本土卡车订单', query(base, { customerId }), payload)
const createdSummary = summary(created.data)
const orderNo = id(createdSummary?.orderNo)
context.assert(MODULE, '卡车创建返回本轮业务键和待审核状态', created.response,
Boolean(orderNo) && id(createdSummary?.customerId) === customerId && createdSummary?.status === 'PENDING_REVIEW' && Number(createdSummary?.version) === 0,
`orderNo=${orderNo || 'missing'}, customerId=${id(createdSummary?.customerId) || 'missing'}, status=${createdSummary?.status || 'missing'}; ${trace(created.response)}`, OPTIONS)
if (!orderNo) return
const registry = ownedRegistry(context)
registry.truck = { marker: context.marker, orderNo, customerId, id: id(createdSummary?.id), finalStatus: createdSummary.status, retained: true }
const path = query(`${base}/${encodeURIComponent(orderNo)}`, { customerId })
try {
const detail = await overseas(context, 'GET', '查询 marker 本土卡车详情', path)
const before = summary(detail.data)
const editedRemark = `${context.marker}-truck-edited`
const edited = await overseas(context, 'PATCH', '编辑 marker 本土卡车订单', path,
{ version: Number(before?.version), destination: `${payload.destination}-EDIT`, remark: editedRemark })
const after = summary(edited.data)
context.assert(MODULE, '卡车编辑版本递增且普通字段回显', edited.response,
after?.destination === `${payload.destination}-EDIT` && Number(after?.version) === Number(before?.version) + 1 && after?.status === 'PENDING_REVIEW',
`version=${after?.version ?? 'missing'}, destination=${after?.destination || 'missing'}; ${trace(edited.response)}`, OPTIONS)
const stale = await context.requestOverseas('PATCH', path, { version: Number(before?.version), destination: 'STALE-DO-NOT-APPLY' })
context.addStep(MODULE, '卡车旧 version 编辑返回冲突', stale, conflict(stale), trace(stale), OPTIONS)
const final = await overseas(context, 'GET', '冲突后回查 marker 本土卡车订单', path)
const finalSummary = summary(final.data)
context.assert(MODULE, '卡车冲突未造成部分更新', final.response,
finalSummary?.destination === after?.destination && Number(finalSummary?.version) === Number(after?.version),
`destination=${finalSummary?.destination || 'missing'}, version=${finalSummary?.version ?? 'missing'}; ${trace(final.response)}`, OPTIONS)
registry.truck.version = Number(finalSummary?.version); registry.truck.finalStatus = finalSummary?.status
} finally {
await cancelMarkerOrder(context, registry.truck, base, 'PENDING_REVIEW', '卡车')
}
}
function looseboardPayload(context) {
const suffix = context.marker.replace(/[^A-Za-z0-9]/g, '').slice(-16)
return {
transportMode: 'SHARED_TRUCK', palletRequired: 'YES', transportType: 'WAREHOUSE_TO_WAREHOUSE',
origin: `散板起运-${suffix}`, originAddress: 'Los Angeles', destination: `散板目的-${suffix}`,
destinationAddress: 'Ontario', containerType: '20GP', packageCount: 3, totalWeightKg: 80,
totalVolumeCbm: 1.8, budgetAmount: 66, currency: 'USD', contactName: '散板验收联系人',
contactInfo: '13800000001', requestedDate: new Date(Date.now() + 8 * 86400000).toISOString().slice(0, 10),
remark: context.marker, idempotencyKey: `${context.marker}-phase4-looseboard`.slice(0, 255),
}
}
async function looseboardFlow(context, customerId) {
const base = `${BASE}/local-looseboard-orders`
const payload = looseboardPayload(context)
const created = await overseas(context, 'POST', '创建 marker 本土散板订单', query(base, { customerId }), payload)
const createdSummary = summary(created.data)
const orderNo = id(createdSummary?.orderNo)
context.assert(MODULE, '散板创建返回本轮业务键和待接单状态', created.response,
Boolean(orderNo) && id(createdSummary?.customerId) === customerId && createdSummary?.status === 'PENDING_ACCEPTANCE' && Number(createdSummary?.version) === 0,
`orderNo=${orderNo || 'missing'}, customerId=${id(createdSummary?.customerId) || 'missing'}, status=${createdSummary?.status || 'missing'}; ${trace(created.response)}`, OPTIONS)
if (!orderNo) return
const registry = ownedRegistry(context)
registry.looseboard = { marker: context.marker, orderNo, customerId, id: id(createdSummary?.id), finalStatus: createdSummary.status, retained: true }
const path = query(`${base}/${encodeURIComponent(orderNo)}`, { customerId })
try {
const detail = await overseas(context, 'GET', '查询 marker 本土散板详情', path)
const before = summary(detail.data)
const { idempotencyKey: _key, ...editable } = payload
const edited = await overseas(context, 'PUT', '编辑 marker 本土散板订单', path,
{ ...editable, version: Number(before?.version), destination: `${payload.destination}-EDIT`, remark: `${context.marker}-looseboard-edited` })
const after = summary(edited.data)
context.assert(MODULE, '散板编辑版本递增且普通字段回显', edited.response,
after?.destination === `${payload.destination}-EDIT` && Number(after?.version) === Number(before?.version) + 1 && after?.status === 'PENDING_ACCEPTANCE',
`version=${after?.version ?? 'missing'}, destination=${after?.destination || 'missing'}; ${trace(edited.response)}`, OPTIONS)
const stale = await context.requestOverseas('PUT', path,
{ ...editable, version: Number(before?.version), destination: 'STALE-DO-NOT-APPLY' })
context.addStep(MODULE, '散板旧 version 编辑返回冲突', stale, conflict(stale), trace(stale), OPTIONS)
const final = await overseas(context, 'GET', '冲突后回查 marker 本土散板订单', path)
const finalSummary = summary(final.data)
context.assert(MODULE, '散板冲突未造成部分更新', final.response,
finalSummary?.destination === after?.destination && Number(finalSummary?.version) === Number(after?.version),
`destination=${finalSummary?.destination || 'missing'}, version=${finalSummary?.version ?? 'missing'}; ${trace(final.response)}`, OPTIONS)
registry.looseboard.version = Number(finalSummary?.version); registry.looseboard.finalStatus = finalSummary?.status
} finally {
await cancelMarkerOrder(context, registry.looseboard, base, 'PENDING_ACCEPTANCE', '散板')
}
}
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)
}