Files
baiyue-firstleg/scripts/admin-test-env-modules/phase4-overseas-business.mjs
T

304 lines
23 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 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)
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 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,
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, 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
}
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 }))
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`,
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) {
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
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)
}
}
}