2026-08-08 10:11:04 +08:00
|
|
|
import { createHash } from 'node:crypto'
|
|
|
|
|
import { readFile } from 'node:fs/promises'
|
|
|
|
|
import { basename } from 'node:path'
|
|
|
|
|
|
|
|
|
|
const PRICE_BOOK_PATH = '/api/platform/v1/fba-pricing/carrier-rates/price-books'
|
|
|
|
|
const DOMESTIC_WAREHOUSE_PATH = '/api/platform/v1/warehouse-admin?page=1&size=200&status=ACTIVE&warehouseType=DOMESTIC'
|
|
|
|
|
const OVERSEAS_WAREHOUSE_PATH = '/api/platform/v1/warehouse-admin?page=1&size=200&status=ACTIVE&warehouseType=OVERSEAS'
|
|
|
|
|
const EXPECTED_CHANNEL_CODES = [
|
|
|
|
|
'MATSON_CLX_TRUCK', 'MATSON_MAX_TRUCK', 'EXX_TRUCK', 'HEDE_TRUCK', 'EXX_RAIL_NY',
|
|
|
|
|
'OA_LA_TRUCK', 'OA_LA_FULL_TRUCK', 'OA_CHICAGO_RAIL', 'OA_HOUSTON_TRUCK',
|
|
|
|
|
'OA_NEW_YORK_TRUCK', 'OA_SAVANNAH_TRUCK', 'MATSON_CLX_PARCEL',
|
|
|
|
|
'MATSON_MAX_PARCEL', 'HEDE_PARCEL', 'OA_PARCEL',
|
|
|
|
|
]
|
|
|
|
|
const MAPPING_ISSUE_CODES = new Set([
|
|
|
|
|
'ORIGIN_MAPPING_REQUIRED',
|
|
|
|
|
'ORIGIN_WAREHOUSE_INVALID',
|
|
|
|
|
'OVERSEAS_MAPPING_REQUIRED',
|
|
|
|
|
'OVERSEAS_WAREHOUSE_INVALID',
|
|
|
|
|
'WAREHOUSE_VALIDATION_FAILED',
|
|
|
|
|
'DUPLICATE_ORIGIN_MAPPING',
|
|
|
|
|
'DUPLICATE_OVERSEAS_MAPPING',
|
|
|
|
|
'UNUSED_ORIGIN_MAPPING',
|
|
|
|
|
'UNUSED_OVERSEAS_MAPPING',
|
|
|
|
|
'DUPLICATE_WAREHOUSE_MAPPING',
|
|
|
|
|
'DUPLICATE_RATE',
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
function sha256(bytes) {
|
|
|
|
|
return createHash('sha256').update(bytes).digest('hex')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalize(value) {
|
|
|
|
|
return String(value || '').trim().toUpperCase()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validId(value) {
|
|
|
|
|
return typeof value === 'string' && /^[1-9][0-9]*$/.test(value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function finishScenario(context, module, scenario, start) {
|
|
|
|
|
const steps = context.steps.slice(start)
|
|
|
|
|
context.scenarios.push({
|
|
|
|
|
name: scenario,
|
|
|
|
|
module,
|
|
|
|
|
success: steps.every((step) => step.passed),
|
|
|
|
|
deploymentBlocked: steps.some((step) => step.deploymentBlocker),
|
|
|
|
|
passed: steps.filter((step) => step.passed).length,
|
|
|
|
|
failed: steps.filter((step) => !step.passed).length,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function uploadWorkbook(context, bytes, fileName) {
|
|
|
|
|
const form = new FormData()
|
|
|
|
|
form.append('file', new Blob([bytes], {
|
|
|
|
|
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
|
|
|
}), fileName)
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`${context.options.gateway}${PRICE_BOOK_PATH}`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { Accept: 'application/json', Authorization: `Bearer ${context.token}` },
|
|
|
|
|
body: form,
|
|
|
|
|
signal: AbortSignal.timeout(120_000),
|
|
|
|
|
})
|
|
|
|
|
const content = await response.text()
|
|
|
|
|
let payload = null
|
|
|
|
|
try { payload = content ? JSON.parse(content) : null } catch { /* Raw response is diagnostic evidence. */ }
|
|
|
|
|
return {
|
|
|
|
|
method: 'POST', path: PRICE_BOOK_PATH, http: response.status, body: payload,
|
|
|
|
|
raw: payload === null ? content.slice(0, 500) : null,
|
|
|
|
|
requestId: payload?.requestId || response.headers.get('x-request-id'),
|
|
|
|
|
traceId: payload?.traceId || response.headers.get('x-trace-id'),
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return {
|
|
|
|
|
method: 'POST', path: PRICE_BOOK_PATH, http: 0, body: null,
|
|
|
|
|
raw: error instanceof Error ? error.message : String(error),
|
|
|
|
|
requestId: null, traceId: null,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildMappings(detail, warehouses) {
|
|
|
|
|
const domestic = warehouses.filter((warehouse) => warehouse.status === 'ACTIVE' && warehouse.warehouseType === 'DOMESTIC')
|
|
|
|
|
const overseas = warehouses.filter((warehouse) => warehouse.status === 'ACTIVE' && warehouse.warehouseType === 'OVERSEAS')
|
|
|
|
|
const originSources = new Map()
|
|
|
|
|
const overseasSources = new Map()
|
|
|
|
|
|
|
|
|
|
for (const rate of detail.rates || []) {
|
|
|
|
|
const originCode = String(rate.originWarehouseCode || '').trim()
|
|
|
|
|
if (originCode && !originSources.has(normalize(originCode))) originSources.set(normalize(originCode), originCode)
|
|
|
|
|
if (rate.destinationType === 'OVERSEAS' && String(rate.destinationName || '').trim()) {
|
|
|
|
|
const sourceName = String(rate.destinationName).trim()
|
|
|
|
|
const key = normalize(sourceName)
|
|
|
|
|
if (!overseasSources.has(key)) overseasSources.set(key, {
|
|
|
|
|
sourceName,
|
|
|
|
|
destinationCode: String(rate.destinationCode || '').trim(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const originMappings = []
|
|
|
|
|
const missingOrigins = []
|
|
|
|
|
for (const sourceCode of originSources.values()) {
|
|
|
|
|
const matches = domestic.filter((warehouse) => normalize(warehouse.warehouseCode) === normalize(sourceCode))
|
|
|
|
|
if (matches.length === 1) originMappings.push({ originWarehouseCode: sourceCode, warehouseId: String(matches[0].id) })
|
|
|
|
|
else missingOrigins.push(`${sourceCode}:${matches.length}`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const overseasMappings = []
|
|
|
|
|
const missingOverseas = []
|
|
|
|
|
for (const source of overseasSources.values()) {
|
|
|
|
|
const matches = overseas.filter((warehouse) => (
|
|
|
|
|
source.destinationCode
|
|
|
|
|
? normalize(warehouse.warehouseCode) === normalize(source.destinationCode)
|
|
|
|
|
: normalize(warehouse.warehouseName) === normalize(source.sourceName)
|
|
|
|
|
))
|
|
|
|
|
if (matches.length === 1) overseasMappings.push({ sourceDestinationName: source.sourceName, warehouseId: String(matches[0].id) })
|
|
|
|
|
else missingOverseas.push(`${source.sourceName}/${source.destinationCode || '-'}:${matches.length}`)
|
|
|
|
|
}
|
|
|
|
|
return { originMappings, overseasMappings, missingOrigins, missingOverseas, originSources, overseasSources }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function run(context) {
|
|
|
|
|
const module = 'pricing'
|
|
|
|
|
const scenario = 'price-book-lifecycle'
|
|
|
|
|
const start = context.steps.length
|
|
|
|
|
const blocker = { scenario, deploymentBlocker: true }
|
|
|
|
|
|
|
|
|
|
const listBeforeResponse = await context.request('GET', PRICE_BOOK_PATH)
|
|
|
|
|
const versionsBefore = context.apiSuccess(module, '查询发布前船期价格版本', listBeforeResponse, '', blocker)
|
|
|
|
|
const activeBefore = Array.isArray(versionsBefore) ? versionsBefore.filter((version) => version.status === 'ACTIVE') : []
|
|
|
|
|
context.assert(module, '发布前最多一份生效版本', listBeforeResponse,
|
|
|
|
|
Array.isArray(versionsBefore) && activeBefore.length <= 1,
|
|
|
|
|
`versions=${Array.isArray(versionsBefore) ? versionsBefore.length : 'n/a'}, active=${activeBefore.length}`, blocker)
|
|
|
|
|
|
2026-08-25 00:08:59 +08:00
|
|
|
const sourcePath = process.env.ADMIN_TEST_PRICE_BOOK_FILE
|
2026-08-08 10:11:04 +08:00
|
|
|
let sourceBytes
|
2026-08-25 00:08:59 +08:00
|
|
|
let sourceName
|
|
|
|
|
if (sourcePath) {
|
|
|
|
|
try {
|
|
|
|
|
sourceBytes = await readFile(sourcePath)
|
|
|
|
|
sourceName = basename(sourcePath)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
context.addStep(module, '读取显式船期价格测试文件', null, false,
|
|
|
|
|
`path=${sourcePath}, error=${error instanceof Error ? error.message : String(error)}`, blocker)
|
|
|
|
|
finishScenario(context, module, scenario, start)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
const activeFixture = activeBefore[0]
|
|
|
|
|
const sourceResponse = activeFixture?.id
|
|
|
|
|
? await context.requestBinary('GET', `${PRICE_BOOK_PATH}/${encodeURIComponent(activeFixture.id)}/source`)
|
|
|
|
|
: null
|
|
|
|
|
const zipMagic = sourceResponse?.bytes?.[0] === 0x50 && sourceResponse?.bytes?.[1] === 0x4b
|
|
|
|
|
const fixtureReady = sourceResponse?.http === 200 && sourceResponse.bytes?.length > 0 && zipMagic
|
|
|
|
|
context.addStep(module, '复用当前生效价格源作为本轮隔离上传夹具', sourceResponse, fixtureReady,
|
|
|
|
|
`activeVersionId=${activeFixture?.id || 'missing'}, bytes=${sourceResponse?.bytes?.length || 0}, zipMagic=${zipMagic}`, blocker)
|
|
|
|
|
if (!fixtureReady) {
|
|
|
|
|
finishScenario(context, module, scenario, start)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
sourceBytes = sourceResponse.bytes
|
|
|
|
|
sourceName = activeFixture.originalFileName || `price-book-${activeFixture.id}.xlsx`
|
2026-08-08 10:11:04 +08:00
|
|
|
}
|
|
|
|
|
const sourceHash = sha256(sourceBytes)
|
2026-08-25 00:08:59 +08:00
|
|
|
const uploadName = `${context.marker}-${sourceName}`
|
2026-08-08 10:11:04 +08:00
|
|
|
context.addStep(module, '船期价格测试文件已隔离', null,
|
|
|
|
|
sourceBytes.length > 0 && uploadName.includes(context.marker) && uploadName.toLowerCase().endsWith('.xlsx'),
|
|
|
|
|
`fileName=${uploadName}, bytes=${sourceBytes.length}, sha256=${sourceHash}`, blocker)
|
|
|
|
|
|
|
|
|
|
const uploadResponse = await uploadWorkbook(context, sourceBytes, uploadName)
|
|
|
|
|
const uploaded = context.apiSuccess(module, '上传并解析船期价格表', uploadResponse,
|
|
|
|
|
`fileName=${uploadName}, bytes=${sourceBytes.length}, sha256=${sourceHash}`, blocker)
|
|
|
|
|
if (!uploaded?.version) {
|
|
|
|
|
finishScenario(context, module, scenario, start)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const versionId = String(uploaded.version.id || '')
|
|
|
|
|
context.createdResources.priceBookVersion = { versionId, marker: context.marker, sourceSha256: sourceHash }
|
|
|
|
|
context.assert(module, '上传返回隔离的待确认版本', uploadResponse,
|
|
|
|
|
validId(versionId)
|
|
|
|
|
&& uploaded.version.status === 'PENDING_CONFIRMATION'
|
|
|
|
|
&& uploaded.version.originalFileName === uploadName
|
|
|
|
|
&& uploaded.version.sourceSha256 === sourceHash,
|
|
|
|
|
`versionId=${versionId}, status=${uploaded.version.status}, fileName=${uploaded.version.originalFileName}, shaMatched=${uploaded.version.sourceSha256 === sourceHash}`, blocker)
|
|
|
|
|
|
|
|
|
|
const channels = Array.isArray(uploaded.channels) ? uploaded.channels : []
|
|
|
|
|
const rates = Array.isArray(uploaded.rates) ? uploaded.rates : []
|
|
|
|
|
const issues = Array.isArray(uploaded.issues) ? uploaded.issues : []
|
|
|
|
|
const channelCodes = channels.map((channel) => String(channel.channelCode))
|
|
|
|
|
const channelSet = new Set(channelCodes)
|
|
|
|
|
const expectedChannels = EXPECTED_CHANNEL_CODES.every((code) => channelSet.has(code))
|
|
|
|
|
context.assert(module, '解析出15个固定渠道且编码唯一', uploadResponse,
|
|
|
|
|
channels.length === EXPECTED_CHANNEL_CODES.length && channelSet.size === channels.length && expectedChannels
|
|
|
|
|
&& Number(uploaded.version.channelCount) === channels.length,
|
|
|
|
|
`channelCount=${channels.length}, unique=${channelSet.size}, missing=${EXPECTED_CHANNEL_CODES.filter((code) => !channelSet.has(code)).join(',') || 'none'}`, blocker)
|
|
|
|
|
const ratesValid = rates.length > 0 && rates.every((rate) => (
|
|
|
|
|
channelSet.has(String(rate.channelCode))
|
|
|
|
|
&& ['FBA_WAREHOUSE_RATE', 'REGION_WEIGHT_TIER'].includes(String(rate.priceModel))
|
|
|
|
|
&& String(rate.originWarehouseCode || '').trim()
|
|
|
|
|
&& ['KG', 'CBM'].includes(String(rate.chargeUnit))
|
|
|
|
|
&& Number(rate.unitPrice) > 0
|
|
|
|
|
&& /^[A-Z]{3}$/.test(String(rate.currency || ''))
|
|
|
|
|
&& String(rate.sourceSheetName || '').trim()
|
|
|
|
|
&& Number(rate.sourceRowNumber) >= 1
|
|
|
|
|
))
|
|
|
|
|
context.assert(module, '解析价格行完整且可追溯到 Excel', uploadResponse,
|
|
|
|
|
ratesValid && Number(uploaded.version.rateCount) === rates.length,
|
|
|
|
|
`rateCount=${rates.length}, declared=${uploaded.version.rateCount}, valid=${ratesValid}`, blocker)
|
|
|
|
|
|
|
|
|
|
const blockingIssues = issues.filter((issue) => issue.severity === 'BLOCKING')
|
|
|
|
|
const unexpectedBlocking = blockingIssues.filter((issue) => !MAPPING_ISSUE_CODES.has(issue.issueCode))
|
|
|
|
|
const infrastructureIssue = issues.find((issue) => issue.issueCode === 'UPLOAD_OR_PARSE_FAILED'
|
|
|
|
|
|| /SQLSyntaxError|bad SQL grammar|Service Unavailable/i.test(String(issue.message || '')))
|
|
|
|
|
context.assert(module, '解析阻断仅包含可确认的仓库映射问题', uploadResponse,
|
|
|
|
|
!infrastructureIssue && unexpectedBlocking.length === 0
|
|
|
|
|
&& Number(uploaded.version.blockingErrorCount) === blockingIssues.length,
|
|
|
|
|
`blocking=${blockingIssues.length}, unexpected=${[...new Set(unexpectedBlocking.map((issue) => issue.issueCode))].join(',') || 'none'}, infrastructure=${infrastructureIssue?.issueCode || 'none'}`, blocker)
|
|
|
|
|
|
|
|
|
|
const listPendingResponse = await context.request('GET', PRICE_BOOK_PATH)
|
|
|
|
|
const versionsPending = context.apiSuccess(module, '上传后回查价格版本列表', listPendingResponse, '', blocker)
|
|
|
|
|
const pendingEntry = versionsPending?.find?.((version) => String(version.id) === versionId)
|
|
|
|
|
const activeAfterUpload = versionsPending?.filter?.((version) => version.status === 'ACTIVE') || []
|
|
|
|
|
context.assert(module, '上传只生成待确认版本且不替换当前生效版本', listPendingResponse,
|
|
|
|
|
pendingEntry?.status === 'PENDING_CONFIRMATION'
|
|
|
|
|
&& activeAfterUpload.length === activeBefore.length
|
|
|
|
|
&& activeBefore.every((before) => activeAfterUpload.some((after) => String(after.id) === String(before.id))),
|
|
|
|
|
`pending=${pendingEntry?.status || 'missing'}, activeBefore=${activeBefore.map((item) => item.id).join(',') || 'none'}, activeAfterUpload=${activeAfterUpload.map((item) => item.id).join(',') || 'none'}`, blocker)
|
|
|
|
|
|
|
|
|
|
const detailResponse = await context.request('GET', `${PRICE_BOOK_PATH}/${encodeURIComponent(versionId)}`)
|
|
|
|
|
const detail = context.apiSuccess(module, '查询待确认版本渠道和价格预览', detailResponse, '', blocker)
|
|
|
|
|
context.assert(module, '详情与上传解析结果一致', detailResponse,
|
|
|
|
|
String(detail?.version?.id) === versionId
|
|
|
|
|
&& detail?.version?.status === 'PENDING_CONFIRMATION'
|
|
|
|
|
&& detail?.channels?.length === channels.length
|
|
|
|
|
&& detail?.rates?.length === rates.length
|
|
|
|
|
&& detail?.issues?.length === issues.length,
|
|
|
|
|
`versionId=${detail?.version?.id || 'missing'}, channels=${detail?.channels?.length ?? 'n/a'}, rates=${detail?.rates?.length ?? 'n/a'}, issues=${detail?.issues?.length ?? 'n/a'}`, blocker)
|
|
|
|
|
|
|
|
|
|
const sourceResponse = await context.requestBinary('GET', `${PRICE_BOOK_PATH}/${encodeURIComponent(versionId)}/source`)
|
|
|
|
|
const downloadedHash = sourceResponse.bytes?.length ? sha256(sourceResponse.bytes) : ''
|
|
|
|
|
const zipMagic = sourceResponse.bytes?.[0] === 0x50 && sourceResponse.bytes?.[1] === 0x4b
|
|
|
|
|
context.addStep(module, '下载原始 XLSX 且内容散列一致', sourceResponse,
|
|
|
|
|
sourceResponse.http === 200 && sourceResponse.bytes?.length === sourceBytes.length
|
|
|
|
|
&& zipMagic && downloadedHash === sourceHash
|
|
|
|
|
&& !sourceResponse.contentType.toLowerCase().includes('json'),
|
|
|
|
|
`bytes=${sourceResponse.bytes?.length ?? 0}, contentType=${sourceResponse.contentType || 'missing'}, zipMagic=${zipMagic}, sha256=${downloadedHash}`, blocker)
|
|
|
|
|
|
|
|
|
|
if (!detail || unexpectedBlocking.length > 0 || infrastructureIssue) {
|
|
|
|
|
finishScenario(context, module, scenario, start)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const domesticResponse = await context.request('GET', DOMESTIC_WAREHOUSE_PATH)
|
|
|
|
|
const domesticPage = context.apiSuccess(module, '查询启用的平台公共国内仓', domesticResponse, '', blocker)
|
|
|
|
|
const overseasResponse = await context.request('GET', OVERSEAS_WAREHOUSE_PATH)
|
|
|
|
|
const overseasPage = context.apiSuccess(module, '查询启用的平台公共海外仓', overseasResponse, '', blocker)
|
|
|
|
|
const warehouses = [
|
|
|
|
|
...(Array.isArray(domesticPage?.records) ? domesticPage.records : []),
|
|
|
|
|
...(Array.isArray(overseasPage?.records) ? overseasPage.records : []),
|
|
|
|
|
]
|
|
|
|
|
const mappings = buildMappings(detail, warehouses)
|
|
|
|
|
const mappingsComplete = mappings.originSources.size > 0
|
|
|
|
|
&& mappings.originMappings.length === mappings.originSources.size
|
|
|
|
|
&& mappings.overseasMappings.length === mappings.overseasSources.size
|
|
|
|
|
&& mappings.missingOrigins.length === 0
|
|
|
|
|
&& mappings.missingOverseas.length === 0
|
|
|
|
|
context.assert(module, '国内起运仓和目的海外仓均精确映射', overseasResponse,
|
|
|
|
|
mappingsComplete,
|
|
|
|
|
`origins=${mappings.originMappings.map((item) => `${item.originWarehouseCode}->${item.warehouseId}`).join(',') || 'none'}, overseas=${mappings.overseasMappings.map((item) => `${item.sourceDestinationName}->${item.warehouseId}`).join(',') || 'none'}, missing=${[...mappings.missingOrigins, ...mappings.missingOverseas].join(',') || 'none'}`, blocker)
|
|
|
|
|
if (!mappingsComplete) {
|
|
|
|
|
finishScenario(context, module, scenario, start)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const publishResponse = await context.request('POST', `${PRICE_BOOK_PATH}/${encodeURIComponent(versionId)}/publish`, {
|
|
|
|
|
originMappings: mappings.originMappings,
|
|
|
|
|
overseasMappings: mappings.overseasMappings,
|
|
|
|
|
})
|
|
|
|
|
const published = context.apiSuccess(module, '确认仓库映射并立即生效', publishResponse,
|
|
|
|
|
`versionId=${versionId}, originMappings=${mappings.originMappings.length}, overseasMappings=${mappings.overseasMappings.length}`, blocker)
|
|
|
|
|
context.assert(module, '发布后价格版本为 ACTIVE 且无阻断', publishResponse,
|
|
|
|
|
published?.version?.status === 'ACTIVE'
|
|
|
|
|
&& Number(published?.version?.blockingErrorCount) === 0
|
|
|
|
|
&& Boolean(published?.version?.publishedAt)
|
|
|
|
|
&& published?.channels?.length === channels.length
|
|
|
|
|
&& published?.rates?.length === rates.length
|
|
|
|
|
&& !(published?.issues || []).some((issue) => issue.severity === 'BLOCKING'),
|
|
|
|
|
`status=${published?.version?.status || 'missing'}, blocking=${published?.version?.blockingErrorCount ?? 'n/a'}, publishedAt=${published?.version?.publishedAt || 'missing'}, channels=${published?.channels?.length ?? 'n/a'}, rates=${published?.rates?.length ?? 'n/a'}`, blocker)
|
|
|
|
|
|
|
|
|
|
const listAfterResponse = await context.request('GET', PRICE_BOOK_PATH)
|
|
|
|
|
const versionsAfter = context.apiSuccess(module, '发布后回查当前生效和历史版本', listAfterResponse, '', blocker)
|
|
|
|
|
const activeAfter = versionsAfter?.filter?.((version) => version.status === 'ACTIVE') || []
|
|
|
|
|
const oldStatusesCorrect = activeBefore.every((before) => {
|
|
|
|
|
if (String(before.id) === versionId) return true
|
|
|
|
|
const after = versionsAfter?.find?.((candidate) => String(candidate.id) === String(before.id))
|
|
|
|
|
return after?.status === 'SUPERSEDED' && Boolean(after.supersededAt)
|
|
|
|
|
})
|
|
|
|
|
context.assert(module, '始终只有一份当前生效版本', listAfterResponse,
|
|
|
|
|
activeAfter.length === 1 && String(activeAfter[0]?.id) === versionId,
|
|
|
|
|
`active=${activeAfter.map((item) => item.id).join(',') || 'none'}`, blocker)
|
|
|
|
|
context.assert(module, '上一生效版本已保留为历史版本', listAfterResponse,
|
|
|
|
|
oldStatusesCorrect,
|
|
|
|
|
`previous=${activeBefore.map((item) => item.id).join(',') || 'none'}, superseded=${oldStatusesCorrect}`, blocker)
|
|
|
|
|
|
|
|
|
|
const activeDetailResponse = await context.request('GET', `${PRICE_BOOK_PATH}/${encodeURIComponent(versionId)}`)
|
|
|
|
|
const activeDetail = context.apiSuccess(module, '查询当前生效价格版本详情', activeDetailResponse, '', blocker)
|
|
|
|
|
context.assert(module, '当前生效版本预览数据完整', activeDetailResponse,
|
|
|
|
|
activeDetail?.version?.status === 'ACTIVE'
|
|
|
|
|
&& activeDetail?.channels?.length === channels.length
|
|
|
|
|
&& activeDetail?.rates?.length === rates.length,
|
|
|
|
|
`status=${activeDetail?.version?.status || 'missing'}, channels=${activeDetail?.channels?.length ?? 'n/a'}, rates=${activeDetail?.rates?.length ?? 'n/a'}`, blocker)
|
|
|
|
|
|
|
|
|
|
const repeatedPublishResponse = await context.request('POST', `${PRICE_BOOK_PATH}/${encodeURIComponent(versionId)}/publish`, {
|
|
|
|
|
originMappings: mappings.originMappings,
|
|
|
|
|
overseasMappings: mappings.overseasMappings,
|
|
|
|
|
})
|
|
|
|
|
const repeatRejected = repeatedPublishResponse.http === 200
|
|
|
|
|
&& String(repeatedPublishResponse.body?.code) !== '200'
|
|
|
|
|
context.addStep(module, '已生效版本禁止重复发布', repeatedPublishResponse, repeatRejected,
|
|
|
|
|
`HTTP=${repeatedPublishResponse.http}, code=${repeatedPublishResponse.body?.code ?? 'missing'}`, blocker)
|
|
|
|
|
|
|
|
|
|
const finalListResponse = await context.request('GET', PRICE_BOOK_PATH)
|
|
|
|
|
const finalVersions = context.apiSuccess(module, '重复发布被拒绝后回查版本状态', finalListResponse, '', blocker)
|
|
|
|
|
const finalActive = finalVersions?.filter?.((version) => version.status === 'ACTIVE') || []
|
|
|
|
|
context.assert(module, '重复发布未改变唯一生效版本', finalListResponse,
|
|
|
|
|
finalActive.length === 1 && String(finalActive[0]?.id) === versionId,
|
|
|
|
|
`active=${finalActive.map((item) => item.id).join(',') || 'none'}`, blocker)
|
|
|
|
|
|
|
|
|
|
finishScenario(context, module, scenario, start)
|
|
|
|
|
}
|