Files
baiyue-firstleg/scripts/admin-test-env-modules/phase4-overseas-master-data.mjs
T
2026-09-02 13:34:53 +08:00

483 lines
28 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, expectedHeader, expectedTokens) {
const text = response?.bytes instanceof Uint8Array ? new TextDecoder().decode(response.bytes).replace(/^\uFEFF/, '') : ''
const header = text.split(/\r?\n/, 1)[0]
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)
context.assert(MODULE, `${name}表头与marker内容`, response,
header === expectedHeader && expectedTokens.every((token) => text.includes(String(token))),
`header=${header || 'missing'}, expectedTokens=${expectedTokens.join('|')}`, BLOCKER)
}
const optionIds = (values) => Array.isArray(values) ? values.map((item) => asId(item?.warehouseId)).filter(Boolean) : []
function assertForbidden(context, name, response) {
context.assert(MODULE, name, response,
response?.http === 403 || (response?.http === 200 && ['403', 'A0403'].includes(String(response?.body?.code))),
`HTTP=${response?.http ?? 0}, code=${response?.body?.code ?? 'missing'}`, 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: '12135550199',
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)
}
async function captureWarehouseOptionsBeforeCreate(context, resource) {
const response = await context.requestOverseas('GET', `${OVERSEAS_BASE}/overseas-quotes/warehouse-options`)
const options = requireData(context, '创建合作仓前查询报价候选缓存基线', response)
context.assert(MODULE, '合作仓候选基线为规范数组', response,
Array.isArray(options) && options.every((item) => asId(item?.warehouseId)
&& item.warehouseCode && item.warehouseName && /^[A-Z]{2}$/.test(item.countryCode)),
`count=${Array.isArray(options) ? options.length : 'invalid'}`, BLOCKER)
resource.partnerWarehouseOptionsBefore = optionIds(options)
}
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)
const createdId = resource.partnerWarehouse?.id
if (resource.partnerWarehouseOptionsBefore?.length === 0) {
context.assert(MODULE, '空候选不负缓存30分钟', optionsResponse,
optionIds(options).includes(createdId), `createdId=${createdId}, ids=${optionIds(options).join(',') || 'empty'}`, BLOCKER)
}
const refreshResponse = await context.requestOverseas('GET',
`${OVERSEAS_BASE}/overseas-quotes/warehouse-options?refresh=true`)
const refreshed = requireData(context, '强制刷新海外报价合作仓候选', refreshResponse)
const option = Array.isArray(refreshed) ? refreshed.find((item) => asId(item.warehouseId) === createdId) : null
context.assert(MODULE, '刷新后新建合作仓立即可见', refreshResponse, Boolean(option),
`createdId=${createdId}, ids=${optionIds(refreshed).join(',') || 'empty'}`, BLOCKER)
if (!option) throw new Error(`warehouse-options refresh=true 未返回本轮合作仓 ${createdId}`)
const cachedResponse = await context.requestOverseas('GET', `${OVERSEAS_BASE}/overseas-quotes/warehouse-options`)
const cached = requireData(context, '刷新后读取30分钟正缓存', cachedResponse)
context.assert(MODULE, '30分钟正缓存与刷新快照一致', cachedResponse,
JSON.stringify(cached) === JSON.stringify(refreshed),
`refreshed=${optionIds(refreshed).join(',')}, cached=${optionIds(cached).join(',')}`, BLOCKER)
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 listResponse = await context.requestOverseas('GET',
`${OVERSEAS_BASE}/overseas-quotes?page=1&size=20&keyword=${encodeURIComponent(context.marker)}`)
const page = requireData(context, '分页查询marker海外报价', listResponse)
context.assert(MODULE, '报价列表包含本轮长ID与recordVersion', listResponse,
Array.isArray(page?.records) && page.records.some((item) => asId(item.id) === id && recordVersion(item.recordVersion)),
`id=${id}, total=${page?.total ?? '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,
'报价ID,报价名称,合作仓编码,合作仓名称,报价方,币种,生效开始,生效结束,版本号,状态,更新时间',
[id, context.marker])
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 listResponse = await context.requestOverseas('GET',
`${OVERSEAS_BASE}/exchange-rates?page=1&size=20&monthFrom=${month}&monthTo=${month}&status=INACTIVE`)
const page = requireData(context, '分页查询marker月汇率', listResponse)
context.assert(MODULE, '汇率列表包含本轮月份与recordVersion', listResponse,
Array.isArray(page?.records) && page.records.some((item) => item.month === month && recordVersion(item.recordVersion)),
`month=${month}, total=${page?.total ?? 'missing'}`, BLOCKER)
const csv = await context.requestOverseasBinary('GET',
`${OVERSEAS_BASE}/exchange-rates/export?monthFrom=${month}&monthTo=${month}`)
assertBinaryCsv(context, '导出marker月汇率CSV', csv,
'月份,基准币种,报价币种,月平均汇率,状态,更新人,更新时间', [month, 'INACTIVE'])
}
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: '12135550188',
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 listResponse = await context.requestOverseas('GET',
`${OVERSEAS_BASE}/vehicles?page=1&size=20&keyword=${encodeURIComponent(createPayload.plateNumber)}`)
const page = requireData(context, '分页查询marker车辆', listResponse)
context.assert(MODULE, '车辆列表包含本轮长ID与recordVersion', listResponse,
Array.isArray(page?.records) && page.records.some((item) => asId(item.id) === id && recordVersion(item.recordVersion)),
`id=${id}, total=${page?.total ?? 'missing'}`, BLOCKER)
const csv = await context.requestOverseasBinary('GET', `${OVERSEAS_BASE}/vehicles/export?keyword=${encodeURIComponent(createPayload.plateNumber)}`)
assertBinaryCsv(context, '导出marker车辆CSV', csv,
'车辆ID,车牌号,车型,油耗(L/100km),当前公里码(km),司机或外派单位,联系电话,区域,车辆归属,状态,更新时间',
[id, createPayload.plateNumber, 'MAINTENANCE'])
}
async function verifyCanonicalPermissionDenial(context, resource) {
const suffix = context.marker.replace(/[^A-Za-z0-9]/g, '').slice(-18).toUpperCase()
const roleResponse = await context.request('POST', '/api/platform/v1/iam/roles', {
roleCode: `P4_MD_DENY_${suffix}`.slice(0, 64), roleName: `四期主数据拒绝-${suffix}`, description: context.marker,
})
const role = requireData(context, '创建无主数据权限角色', roleResponse)
const roleId = asId(role?.id)
if (!roleId) throw new Error('无主数据权限角色未返回ID')
resource.permissionProbe = { roleId, membershipId: null, roleStatus: 'ACTIVE', userStatus: null }
const phone = await context.findAvailablePhone(MODULE)
const username = `p4_md_deny_${suffix}`.slice(0, 64).toLowerCase()
const password = `P4md${suffix.slice(-8)}A1!`
const userResponse = await context.request('POST', '/api/platform/v1/iam/users', {
username, displayName: `四期主数据拒绝-${suffix}`, phone,
email: `${username}@example.com`, tenantId: '0', roleIds: [roleId], password, status: 'ACTIVE',
})
const user = requireData(context, '创建无主数据权限用户', userResponse)
const membershipId = asId(user?.membershipId)
if (!membershipId) throw new Error('无主数据权限用户未返回membershipId')
resource.permissionProbe.membershipId = membershipId
resource.permissionProbe.userStatus = 'ACTIVE'
const loginResponse = await context.request('POST', '/api/auth/login', { username, password }, '')
const login = requireData(context, '无主数据权限用户登录', loginResponse)
if (!login?.overseasAccessToken) throw new Error('无主数据权限用户未签发海外Token')
const denied = await context.requestOverseas('GET',
`${OVERSEAS_BASE}/overseas-quotes?page=1&size=1`, undefined, {}, login.overseasAccessToken)
assertForbidden(context, '无quote:list权限访问海外报价被拒绝', denied)
}
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
}
}
async function cleanupPermissionProbe(context, resource) {
const probe = resource.permissionProbe
if (!probe) return
if (probe.membershipId && probe.userStatus !== 'DISABLED') {
const response = await context.request('PUT', `/api/platform/v1/iam/users/${probe.membershipId}/status`, { status: 'DISABLED' })
context.apiSuccess(MODULE, '无权限探针用户finally停用', response, `membershipId=${probe.membershipId}`, BLOCKER)
if (success(response)) probe.userStatus = 'DISABLED'
}
if (probe.roleId && probe.roleStatus !== 'DISABLED') {
const response = await context.request('PUT', `/api/platform/v1/iam/roles/${probe.roleId}/status`, { status: 'DISABLED' })
context.apiSuccess(MODULE, '无权限探针角色finally停用', response, `roleId=${probe.roleId}`, BLOCKER)
if (success(response)) probe.roleStatus = 'DISABLED'
}
}
export async function run(context) {
const resource = { marker: context.marker }
context.createdResources.phase4OverseasMasterData = resource
try {
await continueLifecycle(context, '海外报价', async () => {
await captureWarehouseOptionsBeforeCreate(context, resource)
await createAndVerifyPartnerWarehouse(context, resource)
await quoteLifecycle(context, resource)
})
await continueLifecycle(context, '月度汇率', () => exchangeLifecycle(context, resource))
await continueLifecycle(context, '车辆主数据', () => vehicleLifecycle(context, resource))
await continueLifecycle(context, 'canonical权限拒绝', () => verifyCanonicalPermissionDenial(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))
await safeCleanup(context, 'canonical权限探针', () => cleanupPermissionProbe(context, resource))
}
}