108 lines
10 KiB
JavaScript
108 lines
10 KiB
JavaScript
const MODULE = 'suppliers'
|
|
const OPTIONS = { scenario: 'platform-public-suppliers', deploymentBlocker: true }
|
|
const BASE = '/api/platform/v1/suppliers'
|
|
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || ''
|
|
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || ''
|
|
|
|
function ok(response) { return response?.http === 200 && String(response?.body?.code) === '200' }
|
|
function code(response, expected) { return response?.http === 200 && String(response?.body?.code) === expected }
|
|
function records(page) { return Array.isArray(page?.records) ? page.records : [] }
|
|
function id(value) { return value == null ? '' : String(value) }
|
|
function traced(response) { return Boolean(response?.requestId && response?.traceId) }
|
|
function sameCarrierCodes(actual, expected) {
|
|
return Array.isArray(actual) && [...actual].sort().join('|') === [...expected].sort().join('|')
|
|
}
|
|
function query(values = {}) { const p = new URLSearchParams(); for (const [k,v] of Object.entries(values)) if (v !== '' && v != null) p.set(k,String(v)); return `${BASE}${p.size ? `?${p}` : ''}` }
|
|
function payload(marker, overrides = {}) { return { name:`供应商验收-${marker}`,category:'BOOKING',carrierCodes:['MATSON','COSCO'],region:'宁波',legalRepresentative:'验收法人',contactName:'验收联系人',phone:'13800000000',email:'supplier-acceptance@example.com',address:'验收地址',businessLicenseNo:`ACPT${marker}`.slice(0,64),licenseFileId:null,status:'ACTIVE',settlementPeriod:'MONTHLY_30',remark:marker,...overrides } }
|
|
|
|
export async function run(context) {
|
|
if (context.options?.fixedCustomerId) return runExistingSuppliers(context)
|
|
const marker=context.marker.replace(/[^0-9A-Za-z]/g,'').slice(-20)
|
|
const listResponse=await context.request('GET',query({tenantId:0,category:'BOOKING',page:1,size:20}))
|
|
const list=context.apiSuccess(MODULE,'列表分页与订舱筛选',listResponse,'category=BOOKING',OPTIONS)
|
|
context.assert(MODULE,'列表分页结构完整',listResponse,Array.isArray(list?.records)&&Number(list?.page)===1&&Number(list?.size)===20&&Number(list?.total)>=0&&Number(list?.pages)>=0,`records=${records(list).length}, total=${list?.total??'missing'}`,OPTIONS)
|
|
|
|
const statsResponse=await context.request('GET',`${BASE}/stats?tenantId=0`)
|
|
const stats=context.apiSuccess(MODULE,'查询四类供应商统计',statsResponse,'',OPTIONS)
|
|
context.assert(MODULE,'统计字段完整',statsResponse,['booking','customs','trucking','certificate'].every(k=>Number(stats?.[k])>=0),JSON.stringify(stats),OPTIONS)
|
|
|
|
const createBody=payload(marker)
|
|
const createResponse=await context.request('POST',`${BASE}?tenantId=0`,createBody)
|
|
const supplierId=id(context.apiSuccess(MODULE,'新增供应商',createResponse,`name=${createBody.name}`,OPTIONS))
|
|
context.assert(MODULE,'新增返回供应商ID',createResponse,Boolean(supplierId),`supplierId=${supplierId||'missing'}`,OPTIONS)
|
|
if(!supplierId)return
|
|
let inactive=false
|
|
try {
|
|
const detailResponse=await context.request('GET',`${BASE}/${encodeURIComponent(supplierId)}?tenantId=0`)
|
|
const detail=context.apiSuccess(MODULE,'查询供应商详情',detailResponse,'',OPTIONS)
|
|
context.assert(MODULE,'详情字段与平台作用域及多船司关联正确',detailResponse,id(detail?.id)===supplierId&&id(detail?.tenantId)==='0'&&detail?.name===createBody.name&&!Object.hasOwn(detail,'scope')&&sameCarrierCodes(detail?.carrierCodes,['MATSON','COSCO']),`id=${id(detail?.id)}, tenantId=${id(detail?.tenantId)}, carrierCodes=${JSON.stringify(detail?.carrierCodes)}, scopeExposed=${Object.hasOwn(detail||{},'scope')}`,OPTIONS)
|
|
|
|
const duplicateResponse=await context.request('POST',`${BASE}?tenantId=0`,payload(`${marker}D`,{name:createBody.name}))
|
|
context.assert(MODULE,'重复名称返回冲突并保留链路ID',duplicateResponse,code(duplicateResponse,'A0409')&&traced(duplicateResponse),`code=${duplicateResponse.body?.code}, requestId=${duplicateResponse.requestId||'missing'}, traceId=${duplicateResponse.traceId||'missing'}`,OPTIONS)
|
|
|
|
const tenantResponse=await context.request('GET',query({tenantId:1,page:1,size:20}))
|
|
context.assert(MODULE,'tenantId非0被拒绝',tenantResponse,code(tenantResponse,'A0400')&&traced(tenantResponse),`code=${tenantResponse.body?.code}, requestId=${tenantResponse.requestId||'missing'}, traceId=${tenantResponse.traceId||'missing'}`,OPTIONS)
|
|
|
|
const nonBookingCarrierResponse=await context.request('POST',`${BASE}?tenantId=0`,payload(`${marker}NB`,{name:`非订舱供应商非法船司-${marker}`,category:'CUSTOMS',carrierCodes:['MATSON']}))
|
|
context.assert(MODULE,'非BOOKING携带carrierCodes返回A0400并保留链路ID',nonBookingCarrierResponse,code(nonBookingCarrierResponse,'A0400')&&traced(nonBookingCarrierResponse),`code=${nonBookingCarrierResponse.body?.code}, requestId=${nonBookingCarrierResponse.requestId||'missing'}, traceId=${nonBookingCarrierResponse.traceId||'missing'}`,OPTIONS)
|
|
|
|
const merchantLogin=await context.request('POST','/api/auth/login',{loginId:MERCHANT_USERNAME,password:MERCHANT_PASSWORD},'')
|
|
const merchantToken=ok(merchantLogin)?merchantLogin.body?.data?.accessToken:''
|
|
const forbidden=merchantToken?await context.request('GET',query({tenantId:0,page:1,size:20}),undefined,merchantToken):merchantLogin
|
|
context.assert(MODULE,'客户账号调用平台供应商接口被拒绝',forbidden,Boolean(merchantToken)&&(forbidden.http===403||code(forbidden,'A0403'))&&traced(forbidden),`HTTP=${forbidden.http}, code=${forbidden.body?.code??'missing'}, requestId=${forbidden.requestId||'missing'}, traceId=${forbidden.traceId||'missing'}`,OPTIONS)
|
|
|
|
const updateBody=payload(marker,{name:`供应商验收已编辑-${marker}`,contactName:'编辑联系人',settlementPeriod:'MONTHLY_45'})
|
|
const updateResponse=await context.request('PUT',`${BASE}/${encodeURIComponent(supplierId)}?tenantId=0`,updateBody)
|
|
context.apiSuccess(MODULE,'编辑供应商',updateResponse,'',OPTIONS)
|
|
const editedResponse=await context.request('GET',`${BASE}/${encodeURIComponent(supplierId)}?tenantId=0`)
|
|
const edited=context.apiSuccess(MODULE,'回查编辑结果',editedResponse,'',OPTIONS)
|
|
context.assert(MODULE,'编辑字段及多船司关联已持久化',editedResponse,edited?.name===updateBody.name&&!Object.hasOwn(edited,'scope')&&edited?.contactName==='编辑联系人'&&edited?.settlementPeriod==='MONTHLY_45'&&sameCarrierCodes(edited?.carrierCodes,['MATSON','COSCO']),`name=${edited?.name||'missing'}, carrierCodes=${JSON.stringify(edited?.carrierCodes)}, scopeExposed=${Object.hasOwn(edited||{},'scope')}`,OPTIONS)
|
|
|
|
const disableResponse=await context.request('PATCH',`${BASE}/${encodeURIComponent(supplierId)}/status?tenantId=0`,{status:'INACTIVE'})
|
|
context.apiSuccess(MODULE,'停用供应商',disableResponse,'',OPTIONS); inactive=ok(disableResponse)
|
|
const inactiveResponse=await context.request('GET',`${BASE}/${encodeURIComponent(supplierId)}?tenantId=0`)
|
|
const inactiveDetail=context.apiSuccess(MODULE,'回查停用状态',inactiveResponse,'',OPTIONS)
|
|
context.assert(MODULE,'停用状态已持久化',inactiveResponse,inactiveDetail?.status==='INACTIVE',`status=${inactiveDetail?.status||'missing'}`,OPTIONS)
|
|
} finally {
|
|
if(!inactive){const cleanup=await context.request('PATCH',`${BASE}/${encodeURIComponent(supplierId)}/status?tenantId=0`,{status:'INACTIVE'}); inactive=ok(cleanup); context.assert(MODULE,'异常路径停用验收供应商',cleanup,inactive,`supplierId=${supplierId}`,OPTIONS)}
|
|
context.createdResources.suppliers={marker:context.marker,supplierId,finalStatus:inactive?'INACTIVE':'UNKNOWN',retained:true}
|
|
}
|
|
}
|
|
|
|
/** 固定资源模式只读现有主档,生命周期缺口仍单独保留。 */
|
|
async function runExistingSuppliers(context) {
|
|
context.addCoverageGap(MODULE, 'fixed-resource-lifecycle', '供应商新增、编辑、停用生命周期',
|
|
'固定资源模式仅验收现有供应商列表、筛选、统计、详情和作用域拒绝,不修改公共主档',
|
|
'not-implemented', { blocking: false })
|
|
const statsResponse = await context.request('GET', `${BASE}/stats?tenantId=0`)
|
|
const stats = context.apiSuccess(MODULE, '查询四类供应商统计', statsResponse, '', OPTIONS)
|
|
const nonNegativeInteger = value => value !== null && value !== undefined && value !== ''
|
|
&& Number.isSafeInteger(Number(value)) && Number(value) >= 0
|
|
context.assert(MODULE, '统计字段完整', statsResponse,
|
|
['booking', 'customs', 'trucking', 'certificate'].every(key => nonNegativeInteger(stats?.[key])), '', OPTIONS)
|
|
for (const category of ['BOOKING', 'CUSTOMS', 'TRUCKING', 'CERTIFICATE']) {
|
|
const response = await context.request('GET', query({ tenantId: 0, category, status: 'ACTIVE', page: 1, size: 20 }))
|
|
const page = context.apiSuccess(MODULE, `查询已有${category}供应商`, response, '', OPTIONS)
|
|
const rows = records(page)
|
|
context.assert(MODULE, `${category}分页和启用类别筛选正确`, response,
|
|
Array.isArray(page?.records) && Number(page.page) === 1 && Number(page.size) === 20
|
|
&& nonNegativeInteger(page.total) && Number(page.pages) === Math.ceil(Number(page.total) / 20)
|
|
&& rows.length === Math.min(20, Number(page.total))
|
|
&& rows.every(row => id(row.id) && row.category === category && row.status === 'ACTIVE'), '', OPTIONS)
|
|
if (!rows.length) {
|
|
context.addCoverageGap(MODULE, `existing-${category}`, `${category}供应商详情`,
|
|
'没有启用的既有样本;未创建供应商,详情未验收', 'prerequisite', { blocking: false })
|
|
continue
|
|
}
|
|
const supplierId = id(rows[0].id)
|
|
if (!supplierId) continue
|
|
const detailResponse = await context.request('GET', `${BASE}/${encodeURIComponent(supplierId)}?tenantId=0`)
|
|
const detail = context.apiSuccess(MODULE, `回查已有${category}供应商详情`, detailResponse, '', OPTIONS)
|
|
context.assert(MODULE, `${category}详情身份、类别及平台归属一致`, detailResponse,
|
|
id(detail?.id) === supplierId && id(detail?.tenantId) === '0' && detail?.category === category
|
|
&& detail?.status === 'ACTIVE' && !Object.hasOwn(detail || {}, 'scope'), '', OPTIONS)
|
|
}
|
|
const denied = await context.request('GET', query({ tenantId: 1, page: 1, size: 20 }))
|
|
context.assert(MODULE, 'tenantId非0被拒绝', denied, code(denied, 'A0400') && traced(denied), '', OPTIONS)
|
|
}
|