Files
baiyue-firstleg/scripts/admin-test-env-modules/phase4-overseas-master-data.mjs
T
2026-08-21 10:56:51 +08:00

387 lines
21 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-master-data'
const DOMESTIC_BASE = '/api/platform/v1/warehouse-admin'
const OVERSEAS_BASE = '/api/platform/v1/last-leg/master-data'
const BLOCKER = { scenario: 'phase4-overseas-master-data-lifecycle', deploymentBlocker: true }
const success = (response) => response?.http === 200 && String(response?.body?.code) === '200'
const data = (response) => success(response) ? response.body.data : null
const recordVersion = (value) => Number.isInteger(Number(value)) && Number(value) >= 0
const asId = (value) => value == null ? '' : String(value)
function requireData(context, name, response) {
const value = context.apiSuccess(MODULE, name, response, '', BLOCKER)
if (value == null) throw new Error(`${name}失败,生命周期无法继续`)
return value
}
function assertBinaryCsv(context, name, response) {
context.assert(MODULE, name, response,
response?.http === 200
&& String(response.contentType || '').toLowerCase().startsWith('text/csv')
&& response.bytes instanceof Uint8Array && response.bytes.length > 0,
`HTTP=${response?.http ?? 0}, contentType=${response?.contentType || 'missing'}, bytes=${response?.bytes?.length || 0}`,
BLOCKER)
}
async function safeCleanup(context, name, action) {
try {
await action()
} catch (error) {
context.assert(MODULE, `${name} finally停用`, null, false,
error instanceof Error ? error.message : String(error), BLOCKER)
}
}
async function continueLifecycle(context, name, action) {
try {
await action()
} catch (error) {
context.assert(MODULE, `${name}生命周期执行`, null, false,
error instanceof Error ? error.message : String(error), BLOCKER)
}
}
function warehousePayload(marker, edited = false) {
const suffix = marker.replace(/[^A-Za-z0-9]/g, '').slice(-20).toUpperCase()
return {
tenantId: 0,
warehouseCode: `P4-${suffix}`.slice(0, 50),
warehouseName: edited ? `${marker} 合作仓-已编辑` : `${marker} 合作仓`,
warehouseType: 'OVERSEAS_PARTNER',
countryCode: 'US',
timezone: 'UTC-8',
contactName: 'Phase4 QA',
contactPhone: '+1-213-555-0199',
contactEmail: null,
status: 'ACTIVE',
remark: edited ? `${marker} lifecycle edited` : `${marker} lifecycle`,
address: {
addressType: 'WAREHOUSE', countryCode: 'US', stateProvince: 'California',
city: 'Los Angeles', district: 'Commerce', postalCode: '90040',
addressLine1: edited ? '402 Phase Four Test Avenue' : '401 Phase Four Test Avenue',
addressLine2: null, latitude: null, longitude: null,
},
capabilities: [{
capabilityCode: 'STORAGE', capabilityName: 'Storage', enabled: true,
cutoffTime: null, description: `${marker} acceptance capability`,
}],
}
}
async function createAndVerifyPartnerWarehouse(context, resource) {
const initial = warehousePayload(context.marker)
const createResponse = await context.request('POST', DOMESTIC_BASE, initial)
const createdId = asId(requireData(context, '新增OVERSEAS_PARTNER合作仓', createResponse))
if (!createdId) throw new Error('合作仓创建响应未返回ID')
resource.partnerWarehouse = { id: createdId, code: initial.warehouseCode, finalStatus: 'ACTIVE' }
const detailPath = `${DOMESTIC_BASE}/${encodeURIComponent(createdId)}?tenantId=0`
const detailResponse = await context.request('GET', detailPath)
const detail = requireData(context, '查询合作仓详情', detailResponse)
context.assert(MODULE, '合作仓真实类型与marker', detailResponse,
detail?.warehouse?.warehouseType === 'OVERSEAS_PARTNER'
&& detail.warehouse.warehouseCode === initial.warehouseCode,
`id=${createdId}, type=${detail?.warehouse?.warehouseType || 'missing'}, code=${detail?.warehouse?.warehouseCode || 'missing'}`,
BLOCKER)
const edited = warehousePayload(context.marker, true)
const editResponse = await context.request('PUT', detailPath, edited)
context.apiSuccess(MODULE, '编辑合作仓(当前契约无version)', editResponse, 'warehouse SaveRequest 无乐观锁字段', BLOCKER)
const afterResponse = await context.request('GET', detailPath)
const after = requireData(context, '编辑后查询合作仓详情', afterResponse)
context.assert(MODULE, '合作仓编辑持久化', afterResponse,
after?.warehouse?.warehouseName === edited.warehouseName
&& after?.warehouse?.remark === edited.remark
&& after?.warehouse?.warehouseType === 'OVERSEAS_PARTNER',
`name=${after?.warehouse?.warehouseName || 'missing'}, type=${after?.warehouse?.warehouseType || 'missing'}`,
BLOCKER)
}
function quoteCreatePayload(context, warehouseId) {
return {
name: `${context.marker} 海外报价`, warehouseId,
providerName: `${context.marker} Provider`, currency: 'USD',
effectiveStart: '2098-01-01', effectiveEnd: '2098-12-31',
versionCode: `${context.marker}-V1`.slice(0, 64),
warehouseFees: [{ item: 'Storage QA', billingUnit: 'PALLET', unitPrice: 1.25, minimumCharge: 2.5, remark: context.marker }],
transitPrices: [{ fbaWarehouseCode: 'ONT8', deliveryMethod: 'TRUCK', estimatedMiles: 12.5, price: 18.75, minimumCharge: 20, remark: context.marker }],
remark: `${context.marker} lifecycle`,
}
}
function quoteUpdatePayload(created, context, version) {
return {
recordVersion: version, name: `${context.marker} 海外报价-已编辑`,
providerName: created.providerName, currency: created.currency,
effectiveStart: created.effectiveStart, effectiveEnd: created.effectiveEnd,
warehouseFees: [{ item: 'Storage QA Edited', billingUnit: 'PALLET', unitPrice: 1.5, minimumCharge: 3, remark: `${context.marker} edited` }],
transitPrices: created.transitPrices,
remark: `${context.marker} lifecycle edited`,
}
}
async function quoteLifecycle(context, resource) {
const optionsResponse = await context.requestOverseas('GET', `${OVERSEAS_BASE}/overseas-quotes/warehouse-options`)
const options = requireData(context, '查询海外报价合作仓候选', optionsResponse)
context.assert(MODULE, '报价候选只来自海外正式接口', optionsResponse,
Array.isArray(options) && options.every((item) => asId(item.warehouseId)
&& item.warehouseCode && item.warehouseName && item.countryCode),
`count=${Array.isArray(options) ? options.length : 'invalid'}`, BLOCKER)
if (Array.isArray(options) && options.length === 0) {
resource.quote = {
skipped: true,
reason: 'warehouse-options-empty-cache',
finalStatus: null,
}
context.addCoverageGap(MODULE, BLOCKER.scenario, '海外报价完整写生命周期',
'warehouse-options 返回空数组;已创建的 OVERSEAS_PARTNER 可能尚未穿透服务端30分钟空缓存。本轮跳过报价且不猜测 warehouseId,继续验收汇率与车辆。',
'deployment-prerequisite', {
marker: context.marker,
partnerWarehouseId: resource.partnerWarehouse?.id || null,
http: optionsResponse?.http ?? null,
code: optionsResponse?.body?.code ?? null,
requestId: optionsResponse?.requestId ?? null,
traceId: optionsResponse?.traceId ?? null,
})
return false
}
const createdOption = options.find((item) => asId(item.warehouseId) === resource.partnerWarehouse?.id)
const option = createdOption || options[0]
if (!option) throw new Error('海外报价接口未返回可用合作仓候选')
const payload = quoteCreatePayload(context, asId(option.warehouseId))
const createResponse = await context.requestOverseas('POST', `${OVERSEAS_BASE}/overseas-quotes`, payload)
let detail = requireData(context, '新增海外报价', createResponse)
const id = asId(detail?.summary?.id)
if (!id || !recordVersion(detail?.summary?.recordVersion)) throw new Error('报价创建响应缺少字符串ID或recordVersion')
resource.quote = { id, warehouseId: payload.warehouseId, recordVersion: Number(detail.summary.recordVersion), finalStatus: detail.summary.status }
const path = `${OVERSEAS_BASE}/overseas-quotes/${encodeURIComponent(id)}`
const detailResponse = await context.requestOverseas('GET', path)
detail = requireData(context, '查询海外报价详情', detailResponse)
context.assert(MODULE, '报价详情长ID、费用和版本', detailResponse,
asId(detail?.summary?.id) === id && recordVersion(detail?.summary?.recordVersion)
&& Array.isArray(detail?.warehouseFees) && detail.warehouseFees.length > 0,
`id=${asId(detail?.summary?.id) || 'missing'}, version=${detail?.summary?.recordVersion ?? 'missing'}`, BLOCKER)
const staleVersion = Number(detail.summary.recordVersion)
const updatePayload = quoteUpdatePayload({ ...payload, transitPrices: payload.transitPrices }, context, staleVersion)
const updateResponse = await context.requestOverseas('PUT', path, updatePayload)
detail = requireData(context, '编辑海外报价', updateResponse)
resource.quote.recordVersion = Number(detail.summary.recordVersion)
context.assert(MODULE, '报价编辑版本递增', updateResponse,
detail.summary.name === updatePayload.name && resource.quote.recordVersion === staleVersion + 1,
`before=${staleVersion}, after=${resource.quote.recordVersion}`, BLOCKER)
const conflictResponse = await context.requestOverseas('PUT', path, { ...updatePayload, name: `${context.marker} stale` })
context.expectConflict(MODULE, '报价旧recordVersion编辑冲突', conflictResponse, BLOCKER.scenario)
const afterConflictResponse = await context.requestOverseas('GET', path)
detail = requireData(context, '报价冲突后重新查询', afterConflictResponse)
context.assert(MODULE, '报价冲突未覆盖新值', afterConflictResponse,
detail.summary.name === updatePayload.name && Number(detail.summary.recordVersion) === resource.quote.recordVersion,
`name=${detail.summary.name}, version=${detail.summary.recordVersion}`, BLOCKER)
const activateResponse = await context.requestOverseas('PATCH', `${path}/status`, {
status: 'ACTIVE', recordVersion: resource.quote.recordVersion,
})
detail = requireData(context, '启用海外报价', activateResponse)
resource.quote.recordVersion = Number(detail.summary.recordVersion)
resource.quote.finalStatus = detail.summary.status
context.assert(MODULE, '报价状态切换为ACTIVE', activateResponse,
detail.summary.status === 'ACTIVE', `status=${detail.summary.status}`, BLOCKER)
const csv = await context.requestOverseasBinary('GET', `${OVERSEAS_BASE}/overseas-quotes/export?keyword=${encodeURIComponent(context.marker)}`)
assertBinaryCsv(context, '导出海外报价CSV', csv)
return true
}
async function chooseSafeExchangeMonth(context) {
const digits = context.marker.replace(/\D/g, '')
const seed = Number(digits.slice(-4) || '0')
for (let offset = 0; offset < 24; offset += 1) {
const serial = ((seed + offset) % 120)
const month = `${2090 + Math.floor(serial / 12)}-${String((serial % 12) + 1).padStart(2, '0')}`
const path = `${OVERSEAS_BASE}/exchange-rates?page=1&size=20&monthFrom=${month}&monthTo=${month}`
const response = await context.requestOverseas('GET', path)
const page = requireData(context, `探测安全汇率月份${month}`, response)
if (Array.isArray(page?.records) && page.records.length === 0 && Number(page.total) === 0) return month
}
throw new Error('2090-2099安全月份池均已占用,拒绝覆盖既有汇率')
}
async function exchangeLifecycle(context, resource) {
const month = await chooseSafeExchangeMonth(context)
const createPayload = { month, usdToCnyAverage: 7.123456, remark: `${context.marker} safe marker month` }
const createResponse = await context.requestOverseas('POST', `${OVERSEAS_BASE}/exchange-rates`, createPayload)
let rate = requireData(context, '新增安全marker月汇率', createResponse)
resource.exchangeRate = { month, recordVersion: Number(rate.recordVersion), finalStatus: rate.status }
const path = `${OVERSEAS_BASE}/exchange-rates/${encodeURIComponent(month)}`
const detailResponse = await context.requestOverseas('GET', path)
rate = requireData(context, '查询marker月汇率详情', detailResponse)
context.assert(MODULE, '汇率详情月份、decimal与版本', detailResponse,
rate.month === month && Number(rate.usdToCnyAverage) > 0 && recordVersion(rate.recordVersion),
`month=${rate.month}, rate=${rate.usdToCnyAverage}, version=${rate.recordVersion}`, BLOCKER)
const staleVersion = Number(rate.recordVersion)
const updatePayload = { recordVersion: staleVersion, usdToCnyAverage: 7.234567, remark: `${context.marker} overwritten once` }
const updateResponse = await context.requestOverseas('PUT', path, updatePayload)
rate = requireData(context, '覆盖维护marker月汇率', updateResponse)
resource.exchangeRate.recordVersion = Number(rate.recordVersion)
context.assert(MODULE, '汇率覆盖版本递增', updateResponse,
Number(rate.usdToCnyAverage) === updatePayload.usdToCnyAverage
&& resource.exchangeRate.recordVersion === staleVersion + 1,
`before=${staleVersion}, after=${resource.exchangeRate.recordVersion}`, BLOCKER)
const conflictResponse = await context.requestOverseas('PUT', path, { ...updatePayload, usdToCnyAverage: 7.345678 })
context.expectConflict(MODULE, '汇率旧recordVersion覆盖冲突', conflictResponse, BLOCKER.scenario)
const afterConflictResponse = await context.requestOverseas('GET', path)
rate = requireData(context, '汇率冲突后重新查询', afterConflictResponse)
context.assert(MODULE, '汇率冲突未覆盖新值', afterConflictResponse,
Number(rate.usdToCnyAverage) === updatePayload.usdToCnyAverage
&& Number(rate.recordVersion) === resource.exchangeRate.recordVersion,
`rate=${rate.usdToCnyAverage}, version=${rate.recordVersion}`, BLOCKER)
const statusResponse = await context.requestOverseas('PATCH', `${path}/status`, {
status: 'INACTIVE', recordVersion: resource.exchangeRate.recordVersion,
remark: `${context.marker} lifecycle complete`,
})
rate = requireData(context, '停用marker月汇率', statusResponse)
resource.exchangeRate.recordVersion = Number(rate.recordVersion)
resource.exchangeRate.finalStatus = rate.status
context.assert(MODULE, '汇率状态切换为INACTIVE', statusResponse,
rate.status === 'INACTIVE', `status=${rate.status}`, BLOCKER)
const csv = await context.requestOverseasBinary('GET',
`${OVERSEAS_BASE}/exchange-rates/export?monthFrom=${month}&monthTo=${month}`)
assertBinaryCsv(context, '导出marker月汇率CSV', csv)
}
function vehiclePayload(context, edited = false) {
const suffix = context.marker.replace(/[^A-Za-z0-9]/g, '').slice(-18).toUpperCase()
return {
plateNumber: `QA-${suffix}`.slice(0, 64),
model: edited ? 'Phase4 QA Truck Edited' : 'Phase4 QA Truck',
fuelConsumptionPer100Km: edited ? 18.75 : 18.5,
mileageKm: edited ? 101 : 100,
driverOrDispatchCompany: `${context.marker} Carrier`, phone: '+1-213-555-0188',
region: 'OVERSEAS', ownership: 'OWNED',
...(edited ? {} : { status: 'IN_SERVICE' }),
remark: edited ? `${context.marker} lifecycle edited` : `${context.marker} lifecycle`,
}
}
async function vehicleLifecycle(context, resource) {
const createPayload = vehiclePayload(context)
const createResponse = await context.requestOverseas('POST', `${OVERSEAS_BASE}/vehicles`, createPayload)
let vehicle = requireData(context, '新增marker车辆', createResponse)
const id = asId(vehicle.id)
if (!id || !recordVersion(vehicle.recordVersion)) throw new Error('车辆创建响应缺少字符串ID或recordVersion')
resource.vehicle = { id, plateNumber: createPayload.plateNumber, recordVersion: Number(vehicle.recordVersion), finalStatus: vehicle.status }
const path = `${OVERSEAS_BASE}/vehicles/${encodeURIComponent(id)}`
const detailResponse = await context.requestOverseas('GET', path)
vehicle = requireData(context, '查询marker车辆详情', detailResponse)
context.assert(MODULE, '车辆详情长ID、五态字段与版本', detailResponse,
asId(vehicle.id) === id && ['IN_SERVICE', 'DISPATCHED', 'MAINTENANCE', 'INACTIVE', 'RETURNED'].includes(vehicle.status)
&& recordVersion(vehicle.recordVersion),
`id=${asId(vehicle.id)}, status=${vehicle.status}, version=${vehicle.recordVersion}`, BLOCKER)
const staleVersion = Number(vehicle.recordVersion)
const updatePayload = { recordVersion: staleVersion, ...vehiclePayload(context, true) }
const updateResponse = await context.requestOverseas('PUT', path, updatePayload)
vehicle = requireData(context, '编辑marker车辆', updateResponse)
const updatedVersion = Number(vehicle.recordVersion)
const updatedMileage = Number(vehicle.mileageKm)
resource.vehicle.recordVersion = updatedVersion
context.assert(MODULE, '车辆编辑版本递增', updateResponse,
vehicle.model === updatePayload.model && updatedMileage === Number(updatePayload.mileageKm)
&& updatedVersion === Number(staleVersion) + 1,
`before=${staleVersion}, after=${updatedVersion}, mileage=${updatedMileage}`, BLOCKER)
const conflictResponse = await context.requestOverseas('PUT', path, { ...updatePayload, model: 'stale overwrite' })
context.expectConflict(MODULE, '车辆旧recordVersion编辑冲突', conflictResponse, BLOCKER.scenario)
const afterConflictResponse = await context.requestOverseas('GET', path)
vehicle = requireData(context, '车辆冲突后重新查询', afterConflictResponse)
context.assert(MODULE, '车辆冲突未覆盖新值', afterConflictResponse,
vehicle.model === updatePayload.model && Number(vehicle.recordVersion) === resource.vehicle.recordVersion,
`model=${vehicle.model}, version=${vehicle.recordVersion}`, BLOCKER)
const statusResponse = await context.requestOverseas('PATCH', `${path}/status`, {
status: 'MAINTENANCE', recordVersion: resource.vehicle.recordVersion,
})
vehicle = requireData(context, '车辆切换安全维护状态', statusResponse)
resource.vehicle.recordVersion = Number(vehicle.recordVersion)
resource.vehicle.finalStatus = vehicle.status
context.assert(MODULE, '车辆状态切换为MAINTENANCE', statusResponse,
vehicle.status === 'MAINTENANCE', `status=${vehicle.status}`, BLOCKER)
const csv = await context.requestOverseasBinary('GET', `${OVERSEAS_BASE}/vehicles/export?keyword=${encodeURIComponent(createPayload.plateNumber)}`)
assertBinaryCsv(context, '导出marker车辆CSV', csv)
}
async function cleanupPartnerWarehouse(context, resource) {
if (!resource.partnerWarehouse?.id) return
const response = await context.request('PATCH',
`${DOMESTIC_BASE}/${encodeURIComponent(resource.partnerWarehouse.id)}/status?tenantId=0&status=INACTIVE`)
context.apiSuccess(MODULE, '合作仓finally停用', response, `id=${resource.partnerWarehouse.id}`, BLOCKER)
if (success(response)) resource.partnerWarehouse.finalStatus = 'INACTIVE'
}
async function cleanupVersioned(context, resource, key, path, extract) {
const current = resource[key]
if (!current) return
const detailResponse = await context.requestOverseas('GET', path)
const detail = data(detailResponse)
const value = extract(detail)
if (!success(detailResponse) || !value || !recordVersion(value.recordVersion)) {
context.assert(MODULE, `${key} finally查询当前版本`, detailResponse, false,
`HTTP=${detailResponse?.http ?? 0}, code=${detailResponse?.body?.code || 'missing'}`, BLOCKER)
return
}
current.recordVersion = Number(value.recordVersion)
current.finalStatus = value.status
if (value.status === 'INACTIVE') return
const response = await context.requestOverseas('PATCH', `${path}/status`, {
status: 'INACTIVE', recordVersion: current.recordVersion,
...(key === 'exchangeRate' ? { remark: `${context.marker} finally cleanup` } : {}),
})
const stopped = data(response)
const normalized = extract(stopped)
context.apiSuccess(MODULE, `${key} finally停用`, response, `path=${path}`, BLOCKER)
if (normalized) {
current.recordVersion = Number(normalized.recordVersion)
current.finalStatus = normalized.status
}
}
export async function run(context) {
const resource = { marker: context.marker }
context.createdResources.phase4OverseasMasterData = resource
try {
await continueLifecycle(context, '海外报价', async () => {
await createAndVerifyPartnerWarehouse(context, resource)
await quoteLifecycle(context, resource)
})
await continueLifecycle(context, '月度汇率', () => exchangeLifecycle(context, resource))
await continueLifecycle(context, '车辆主数据', () => vehicleLifecycle(context, resource))
} finally {
await safeCleanup(context, '海外报价', async () => {
if (!resource.quote?.id) return
const path = `${OVERSEAS_BASE}/overseas-quotes/${encodeURIComponent(resource.quote.id)}`
await cleanupVersioned(context, resource, 'quote', path, (value) => value?.summary)
})
await safeCleanup(context, '月度汇率', async () => {
if (!resource.exchangeRate?.month) return
const path = `${OVERSEAS_BASE}/exchange-rates/${encodeURIComponent(resource.exchangeRate.month)}`
await cleanupVersioned(context, resource, 'exchangeRate', path, (value) => value)
})
await safeCleanup(context, '车辆', async () => {
if (!resource.vehicle?.id) return
const path = `${OVERSEAS_BASE}/vehicles/${encodeURIComponent(resource.vehicle.id)}`
await cleanupVersioned(context, resource, 'vehicle', path, (value) => value)
})
await safeCleanup(context, '合作仓', () => cleanupPartnerWarehouse(context, resource))
}
}