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) } } }