test: strengthen phase4 overseas business acceptance
This commit is contained in:
@@ -7,107 +7,263 @@ import { run } from './phase4-overseas-business.mjs'
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url))
|
||||
const source = fs.readFileSync(path.join(here, 'phase4-overseas-business.mjs'), 'utf8')
|
||||
function response(data, code = '200') { return { http: 200, body: { code, message: code === '200' ? 'success' : 'conflict', data }, requestId: 'request-test', traceId: 'trace-test' } }
|
||||
function response(data, code = '200') {
|
||||
return { http: 200, body: { code, message: code === '200' ? 'success' : 'conflict', data },
|
||||
requestId: `request-${code}`, traceId: `trace-${code}` }
|
||||
}
|
||||
function page(records = []) { return { records, page: 1, size: 20, total: records.length, pages: records.length ? 1 : 0 } }
|
||||
function blankToNull(value) { return typeof value === 'string' && value.trim() === '' ? null : value }
|
||||
|
||||
function fixture() {
|
||||
const calls = []; const steps = []; const gaps = []
|
||||
function fixture(options = {}) {
|
||||
const calls = []; const steps = []; const gaps = []; const blockers = []
|
||||
const marker = 'HTTP-20260824-001'
|
||||
const state = {
|
||||
truck: null,
|
||||
looseboard: null,
|
||||
purchase: { id: '30001', orderNo: 'OP1', customerId: '10001', purchaseWarehouseId: '20001', purchaseCategory: '家居', productName: '置物架', purchaseQuantity: 2, inboundQuantity: 0, inventoryQuantity: 0, status: 'PENDING_REVIEW', version: 0 },
|
||||
warehouseStatus: 'ACTIVE', purchase: null, purchaseHash: '', truck: null, truckHash: '', looseboard: null, looseboardHash: '',
|
||||
purchaseHistories: [], truckHistories: [], looseboardHistories: [],
|
||||
}
|
||||
const marker = 'HTTP-20260821-001'
|
||||
const activeCustomers = options.activeCustomers ?? [{ id: '10001', tenantName: '客户甲', status: 'ACTIVE' }]
|
||||
const warehouseOptions = options.warehouseOptions ?? [
|
||||
{ warehouseId: '20001', warehouseCode: 'LAX', warehouseName: '洛杉矶仓', countryCode: 'US' },
|
||||
]
|
||||
const credentials = options.credentials === false ? {} : { username: 'merchant', password: 'secret' }
|
||||
|
||||
function purchaseDetail() {
|
||||
return { summary: state.purchase, productUrl: state.purchase?.productUrl ?? null,
|
||||
domesticCarrier: state.purchase?.domesticCarrier ?? null, domesticTrackingNo: state.purchase?.domesticTrackingNo ?? null,
|
||||
estimatedArrivalAt: state.purchase?.estimatedArrivalAt ?? null, remark: state.purchase?.remark ?? null,
|
||||
histories: state.purchaseHistories }
|
||||
}
|
||||
function truckDetail() { return { summary: state.truck, remark: state.truck?.remark ?? null, histories: state.truckHistories } }
|
||||
function looseboardDetail() { return { summary: state.looseboard, remark: state.looseboard?.remark ?? null, histories: state.looseboardHistories } }
|
||||
|
||||
const context = {
|
||||
marker, calls, steps, gaps,
|
||||
context: { merchantQualificationFixture: { tenantId: '10001', username: 'merchant', password: 'secret' } },
|
||||
createdResources: { merchantQualificationFixture: { tenantId: '10001', username: 'merchant', marker } },
|
||||
marker, calls, steps, gaps, blockers,
|
||||
context: { merchantQualificationFixture: credentials },
|
||||
createdResources: { merchantQualificationFixture: { tenantId: '10001', marker } },
|
||||
async request(method, requestPath, body) {
|
||||
calls.push({ channel: 'domestic', method, path: requestPath, body })
|
||||
if (requestPath.includes('/iam/tenants?')) return response(page([{ id: '10001', tenantName: '客户甲', status: 'ACTIVE' }]))
|
||||
if (requestPath.includes('/iam/tenants?')) return response(page(activeCustomers))
|
||||
if (requestPath === '/api/auth/login') return response({ overseasAccessToken: 'merchant-overseas-token' })
|
||||
if (method === 'POST' && requestPath === '/api/platform/v1/warehouse-admin') return response('20001')
|
||||
if (method === 'PATCH' && requestPath.includes('/warehouse-admin/20001/status?')) return response(null)
|
||||
if (method === 'PATCH' && requestPath.includes('/warehouse-admin/20001/status?')) {
|
||||
state.warehouseStatus = 'INACTIVE'; return response(null)
|
||||
}
|
||||
throw new Error(`unexpected domestic ${method} ${requestPath}`)
|
||||
},
|
||||
async requestOverseas(method, requestPath, body, headers, token) {
|
||||
calls.push({ channel: 'overseas', method, path: requestPath, body })
|
||||
calls.push({ channel: 'overseas', method, path: requestPath, body, tokenPresent: Boolean(token) })
|
||||
|
||||
if (requestPath === '/api/merchant/v1/last-leg/overseas-procurements' && method === 'POST') {
|
||||
assert.equal(token, 'merchant-overseas-token')
|
||||
state.purchase = { ...state.purchase, customerName: body.customerName, remark: body.remark }
|
||||
return response({ summary: state.purchase })
|
||||
const hash = JSON.stringify(body)
|
||||
if (state.purchase) return hash === state.purchaseHash ? response(purchaseDetail()) : response(null, 'A0409')
|
||||
state.purchaseHash = hash
|
||||
state.purchase = { id: '30001', orderNo: 'OP1', customerId: '10001', customerName: body.customerName,
|
||||
purchaseWarehouseId: null, purchaseWarehouseName: null, purchaseCategory: body.purchaseCategory,
|
||||
productName: body.productName, productSpecification: body.productSpecification, productUrl: body.productUrl,
|
||||
purchaseQuantity: body.purchaseQuantity, inboundQuantity: 0, inventoryQuantity: 0,
|
||||
status: 'PENDING_REVIEW', version: 0, remark: body.remark }
|
||||
state.purchaseHistories.push({ operationType: 'CREATE', fromStatus: null, toStatus: 'PENDING_REVIEW', remark: marker })
|
||||
return response(purchaseDetail())
|
||||
}
|
||||
if (requestPath.includes('/customer-options')) return response(state.purchase
|
||||
? [{ customerId: '10001', customerName: '客户甲' }] : [])
|
||||
if (requestPath.includes('/warehouse-options')) return response(warehouseOptions)
|
||||
if (requestPath.includes('/overseas-procurements?') && method === 'GET') return response(page(state.purchase ? [state.purchase] : []))
|
||||
if (requestPath.includes('/overseas-procurements/OP1/review?') && method === 'POST') {
|
||||
if (state.purchase.status !== 'PENDING_REVIEW') return response(null, 'A0409')
|
||||
const next = body.decision === 'REJECT' ? 'REJECTED' : 'APPROVED'
|
||||
state.purchase = { ...state.purchase, status: next, version: state.purchase.version + 1 }
|
||||
state.purchaseHistories.push({ operationType: 'REVIEW', fromStatus: 'PENDING_REVIEW', toStatus: next, remark: body.remark })
|
||||
return response(purchaseDetail())
|
||||
}
|
||||
if (requestPath.includes('/overseas-procurements/OP1?') && method === 'GET') return response(purchaseDetail())
|
||||
if (requestPath.includes('/overseas-procurements/OP1?') && method === 'PATCH') {
|
||||
if (Number(body.version) !== state.purchase.version) return response(null, 'A0409')
|
||||
const patch = { ...body }; delete patch.version
|
||||
for (const key of ['productSpecification', 'productUrl', 'domesticCarrier', 'domesticTrackingNo', 'remark']) {
|
||||
if (Object.hasOwn(patch, key)) patch[key] = blankToNull(patch[key])
|
||||
}
|
||||
if (Object.hasOwn(patch, 'purchaseWarehouseId') && blankToNull(patch.purchaseWarehouseId) == null) {
|
||||
patch.purchaseWarehouseId = null; patch.purchaseWarehouseName = null
|
||||
}
|
||||
state.purchase = { ...state.purchase, ...patch, version: state.purchase.version + 1 }
|
||||
state.purchaseHistories.push({ operationType: 'UPDATE', fromStatus: state.purchase.status,
|
||||
toStatus: state.purchase.status, changeDetail: JSON.stringify({ marker }) })
|
||||
return response(purchaseDetail())
|
||||
}
|
||||
|
||||
if (requestPath.includes('/us-truck-orders?') && method === 'POST') {
|
||||
const hash = JSON.stringify(body)
|
||||
if (state.truck) return hash === state.truckHash ? response(truckDetail()) : response(null, 'A0409')
|
||||
state.truckHash = hash
|
||||
state.truck = { id: '40001', orderNo: 'TK1', customerId: '10001', status: 'PENDING_REVIEW', version: 0, ...body }
|
||||
state.truckHistories.push({ fromStatus: null, toStatus: 'PENDING_REVIEW', remark: marker })
|
||||
return response(truckDetail())
|
||||
}
|
||||
if (requestPath.includes('/us-truck-orders?') && method === 'GET') return response(page(state.truck ? [state.truck] : []))
|
||||
if (requestPath.includes('/us-truck-orders/TK1/status?') && method === 'PATCH') {
|
||||
if (Number(body.version) !== state.truck.version) return response(null, 'A0409')
|
||||
const allowed = (state.truck.status === 'PENDING_REVIEW' && ['WAITING_INBOUND', 'CANCELLED'].includes(body.status))
|
||||
|| (state.truck.status === 'WAITING_INBOUND' && body.status === 'CANCELLED')
|
||||
if (!allowed) return response(null, 'A0409')
|
||||
const from = state.truck.status
|
||||
state.truck = { ...state.truck, status: body.status, version: state.truck.version + 1 }
|
||||
state.truckHistories.push({ fromStatus: from, toStatus: body.status, remark: body.remark })
|
||||
return response(truckDetail())
|
||||
}
|
||||
if (requestPath.includes('/us-truck-orders/TK1?') && method === 'GET') return response(truckDetail())
|
||||
if (requestPath.includes('/us-truck-orders/TK1?') && method === 'PATCH') {
|
||||
if (Number(body.version) !== state.truck.version) return response(null, 'A0409')
|
||||
state.truck = { ...state.truck, ...body, version: state.truck.version + 1 }
|
||||
state.truckHistories.push({ fromStatus: state.truck.status, toStatus: state.truck.status, remark: marker })
|
||||
return response(truckDetail())
|
||||
}
|
||||
|
||||
if (requestPath.includes('/local-looseboard-orders?') && method === 'POST') {
|
||||
const hash = JSON.stringify(body)
|
||||
if (state.looseboard) return hash === state.looseboardHash ? response(looseboardDetail()) : response(null, 'A0409')
|
||||
state.looseboardHash = hash
|
||||
state.looseboard = { id: '50001', orderNo: 'LB1', customerId: '10001', status: 'PENDING_ACCEPTANCE', version: 0, ...body }
|
||||
state.looseboardHistories.push({ operationType: 'CREATE', fromStatus: null, toStatus: 'PENDING_ACCEPTANCE', remark: marker })
|
||||
return response(looseboardDetail())
|
||||
}
|
||||
if (requestPath.includes('/local-looseboard-orders?') && method === 'GET') return response(page(state.looseboard ? [state.looseboard] : []))
|
||||
if (requestPath.includes('/local-looseboard-orders/LB1/status?') && method === 'PATCH') {
|
||||
if (Number(body.version) !== state.looseboard.version) return response(null, 'A0409')
|
||||
const allowed = (state.looseboard.status === 'PENDING_ACCEPTANCE' && ['DISPATCHED', 'CANCELLED'].includes(body.status))
|
||||
|| (state.looseboard.status === 'DISPATCHED' && body.status === 'CANCELLED')
|
||||
if (!allowed) return response(null, 'A0409')
|
||||
const from = state.looseboard.status
|
||||
state.looseboard = { ...state.looseboard, status: body.status, version: state.looseboard.version + 1 }
|
||||
state.looseboardHistories.push({ operationType: 'STATUS', fromStatus: from, toStatus: body.status, remark: body.remark })
|
||||
return response(looseboardDetail())
|
||||
}
|
||||
if (requestPath.includes('/local-looseboard-orders/LB1?') && method === 'GET') return response(looseboardDetail())
|
||||
if (requestPath.includes('/local-looseboard-orders/LB1?') && method === 'PUT') {
|
||||
if (Number(body.version) !== state.looseboard.version) return response(null, 'A0409')
|
||||
state.looseboard = { ...state.looseboard, ...body, version: state.looseboard.version + 1 }
|
||||
state.looseboardHistories.push({ operationType: 'UPDATE', fromStatus: state.looseboard.status,
|
||||
toStatus: state.looseboard.status, changeDetail: JSON.stringify({ marker }) })
|
||||
return response(looseboardDetail())
|
||||
}
|
||||
if (requestPath.includes('/customer-options')) return response([{ customerId: '10001', customerName: '客户甲' }])
|
||||
if (requestPath.includes('/warehouse-options')) return response([{ warehouseId: '20001', warehouseCode: 'LAX', warehouseName: '洛杉矶仓', countryCode: 'US' }])
|
||||
if (requestPath.includes('/overseas-procurements?') && method === 'GET') return response(page([state.purchase]))
|
||||
if (requestPath.includes('/overseas-procurements/OP1?') && method === 'GET') return response({ summary: state.purchase, remark: marker })
|
||||
if (requestPath.includes('/overseas-procurements/OP1?') && method === 'PATCH') { state.purchase = { ...state.purchase, ...body, version: state.purchase.version + 1 }; return response({ summary: state.purchase }) }
|
||||
if (requestPath.includes('/overseas-procurements/OP1/review?') && method === 'POST') { state.purchase = { ...state.purchase, status: 'REJECTED', version: state.purchase.version + 1 }; return response({ summary: state.purchase }) }
|
||||
if (requestPath.includes('/us-truck-orders?') && method === 'POST') { state.truck = { id: '40001', orderNo: 'TK1', customerId: '10001', status: 'PENDING_REVIEW', version: 0, ...body }; return response({ summary: state.truck }) }
|
||||
if (requestPath.includes('/us-truck-orders/TK1/status?') && method === 'PATCH') { if (Number(body.version) !== state.truck.version) return response(null, 'A0409'); state.truck = { ...state.truck, status: body.status, version: state.truck.version + 1 }; return response({ summary: state.truck }) }
|
||||
if (requestPath.includes('/us-truck-orders/TK1?') && method === 'GET') return response({ summary: state.truck })
|
||||
if (requestPath.includes('/us-truck-orders/TK1?') && method === 'PATCH') { if (Number(body.version) !== state.truck.version) return response(null, 'A0409'); state.truck = { ...state.truck, ...body, version: state.truck.version + 1 }; return response({ summary: state.truck }) }
|
||||
if (requestPath.includes('/local-looseboard-orders?') && method === 'POST') { state.looseboard = { id: '50001', orderNo: 'LB1', customerId: '10001', status: 'PENDING_ACCEPTANCE', version: 0, ...body }; return response({ summary: state.looseboard }) }
|
||||
if (requestPath.includes('/local-looseboard-orders/LB1/status?') && method === 'PATCH') { if (Number(body.version) !== state.looseboard.version) return response(null, 'A0409'); state.looseboard = { ...state.looseboard, status: body.status, version: state.looseboard.version + 1 }; return response({ summary: state.looseboard }) }
|
||||
if (requestPath.includes('/local-looseboard-orders/LB1?') && method === 'GET') return response({ summary: state.looseboard })
|
||||
if (requestPath.includes('/local-looseboard-orders/LB1?') && method === 'PUT') { if (Number(body.version) !== state.looseboard.version) return response(null, 'A0409'); state.looseboard = { ...state.looseboard, ...body, version: state.looseboard.version + 1 }; return response({ summary: state.looseboard }) }
|
||||
throw new Error(`unexpected ${method} ${requestPath}`)
|
||||
},
|
||||
apiSuccess(module, name, result, evidence, options) { const passed = result.http === 200 && String(result.body?.code) === '200'; steps.push({ module, name, passed, evidence, options }); return passed ? result.body.data : null },
|
||||
assert(module, name, result, passed, evidence, options) { steps.push({ module, name, passed: Boolean(passed), result, evidence, options }) },
|
||||
addStep(module, name, result, passed, evidence, options) { steps.push({ module, name, passed: Boolean(passed), result, evidence, options }) },
|
||||
apiSuccess(module, name, result, evidence, stepOptions) {
|
||||
const passed = result.http === 200 && String(result.body?.code) === '200'
|
||||
this.addStep(module, name, result, passed, evidence, stepOptions)
|
||||
return passed ? result.body.data : null
|
||||
},
|
||||
assert(module, name, result, passed, evidence, stepOptions) {
|
||||
this.addStep(module, name, result, Boolean(passed), evidence, stepOptions)
|
||||
},
|
||||
addStep(module, name, result, passed, evidence, stepOptions = {}) {
|
||||
const step = { module, name, passed: Boolean(passed), result, evidence, options: stepOptions,
|
||||
requestId: result?.requestId || null, traceId: result?.traceId || null }
|
||||
steps.push(step)
|
||||
if (stepOptions.deploymentBlocker && !step.passed) blockers.push(step)
|
||||
},
|
||||
addCoverageGap(...args) { gaps.push(args) },
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
test('module is context injected and excludes forbidden business surfaces', () => {
|
||||
test('module is context injected, blocks missing prerequisites and excludes forbidden surfaces', () => {
|
||||
assert.match(source, /export async function run\(context\)/)
|
||||
assert.doesNotMatch(source, /https?:\/\/|process\.env|fetch\(/)
|
||||
for (const forbidden of ['/outbounds', '/receive-pallets', '/outbound', '/delivery', '/dispatch', '/export']) assert.equal(source.includes(forbidden), false, forbidden)
|
||||
assert.doesNotMatch(source, /addCoverageGap|coverageGap\(/)
|
||||
assert.match(source, /deploymentBlocker: true/)
|
||||
for (const forbidden of ['/outbounds', '/receive-pallets', '/outbound', '/delivery', '/dispatch', '/export']) {
|
||||
assert.equal(source.includes(forbidden), false, forbidden)
|
||||
}
|
||||
assert.doesNotMatch(source, /requestOverseas\('DELETE'/)
|
||||
})
|
||||
|
||||
test('marker truck and looseboard cover create detail edit stale conflict and finally cancel', async () => {
|
||||
test('complete procurement truck and looseboard lifecycles pass with legal marker terminals', async () => {
|
||||
const context = fixture()
|
||||
await run(context)
|
||||
assert.equal(context.steps.every((step) => step.passed), true, context.steps.filter((step) => !step.passed).map((step) => `${step.name}: ${step.evidence}`).join('\n'))
|
||||
assert.ok(context.calls.some((call) => call.method === 'POST' && call.path === '/api/merchant/v1/last-leg/overseas-procurements'))
|
||||
assert.ok(context.calls.some((call) => call.method === 'POST' && call.path === '/api/platform/v1/last-leg/us-truck-orders?customerId=10001'))
|
||||
assert.ok(context.calls.some((call) => call.method === 'POST' && call.path === '/api/platform/v1/last-leg/local-looseboard-orders?customerId=10001'))
|
||||
assert.equal(context.calls.filter((call) => call.method === 'PATCH' && call.path.includes('/us-truck-orders/')).length, 3)
|
||||
assert.equal(context.calls.filter((call) => call.method === 'PUT' && call.path.includes('/local-looseboard-orders/')).length, 2)
|
||||
assert.equal(context.calls.filter((call) => call.method === 'PATCH' && call.path.includes('/local-looseboard-orders/')).length, 1)
|
||||
const statusCalls = context.calls.filter((call) => call.channel === 'overseas' && call.path.includes('/status?'))
|
||||
assert.deepEqual(statusCalls.map((call) => [call.method, call.path, call.body.status]), [
|
||||
['PATCH', '/api/platform/v1/last-leg/us-truck-orders/TK1/status?customerId=10001', 'CANCELLED'],
|
||||
['PATCH', '/api/platform/v1/last-leg/local-looseboard-orders/LB1/status?customerId=10001', 'CANCELLED'],
|
||||
])
|
||||
assert.ok(statusCalls.every((call) => Number(call.body.version) === 1))
|
||||
assert.equal(context.createdResources.phase4OverseasBusiness.truck.marker, context.marker)
|
||||
assert.equal(context.createdResources.phase4OverseasBusiness.looseboard.marker, context.marker)
|
||||
assert.equal(context.createdResources.phase4OverseasBusiness.truck.finalStatus, 'CANCELLED')
|
||||
assert.equal(context.createdResources.phase4OverseasBusiness.looseboard.finalStatus, 'CANCELLED')
|
||||
assert.equal(context.createdResources.phase4OverseasBusiness.truck.retained, false)
|
||||
assert.equal(context.createdResources.phase4OverseasBusiness.looseboard.retained, false)
|
||||
assert.equal(context.gaps.filter((gap) => gap[4]?.includes('retained-marker-test-data')).length, 0)
|
||||
assert.ok(context.gaps.every((gap) => !/requestId=missing|traceId=missing/.test(gap[3])))
|
||||
assert.equal(context.steps.every((step) => step.passed), true,
|
||||
context.steps.filter((step) => !step.passed).map((step) => `${step.name}: ${step.evidence}`).join('\n'))
|
||||
assert.equal(context.gaps.length, 0)
|
||||
assert.equal(context.blockers.length, 0)
|
||||
|
||||
const procurementWrites = context.calls.filter((call) => call.path.includes('/overseas-procurements') && call.method !== 'GET')
|
||||
assert.deepEqual(procurementWrites.map((call) => call.method),
|
||||
['POST', 'POST', 'POST', 'PATCH', 'PATCH', 'PATCH', 'PATCH', 'POST', 'POST'])
|
||||
assert.equal(procurementWrites.filter((call) => call.method === 'PATCH').length, 4)
|
||||
assert.ok(procurementWrites.some((call) => call.method === 'PATCH' && call.body.productSpecification === null && call.body.productUrl === ''))
|
||||
|
||||
const truckCalls = context.calls.filter((call) => call.path.includes('/us-truck-orders'))
|
||||
assert.equal(truckCalls.filter((call) => call.method === 'POST').length, 3)
|
||||
assert.equal(truckCalls.filter((call) => call.method === 'GET' && call.path.includes('page=1')).length, 1)
|
||||
assert.equal(truckCalls.filter((call) => call.method === 'PATCH' && call.path.includes('/status?')).length, 3)
|
||||
|
||||
const looseboardCalls = context.calls.filter((call) => call.path.includes('/local-looseboard-orders'))
|
||||
assert.equal(looseboardCalls.filter((call) => call.method === 'POST').length, 3)
|
||||
assert.equal(looseboardCalls.filter((call) => call.method === 'GET' && call.path.includes('page=1')).length, 1)
|
||||
assert.equal(looseboardCalls.filter((call) => call.method === 'PUT').length, 2)
|
||||
assert.equal(looseboardCalls.filter((call) => call.method === 'PATCH' && call.path.includes('/status?')).length, 3)
|
||||
|
||||
const registry = context.createdResources.phase4OverseasBusiness
|
||||
assert.equal(registry.purchase.finalStatus, 'REJECTED')
|
||||
assert.equal(registry.purchase.retained, false)
|
||||
assert.equal(registry.truck.finalStatus, 'CANCELLED')
|
||||
assert.equal(registry.truck.retained, false)
|
||||
assert.equal(registry.looseboard.finalStatus, 'CANCELLED')
|
||||
assert.equal(registry.looseboard.retained, false)
|
||||
assert.equal(registry.partnerWarehouse.finalStatus, 'INACTIVE')
|
||||
assert.equal(registry.partnerWarehouse.retained, false)
|
||||
})
|
||||
|
||||
test('marker procurement is created through merchant then edited and rejected by platform', async () => {
|
||||
test('old versions and reverse transitions are exact A0409 conflicts with trace evidence', async () => {
|
||||
const context = fixture()
|
||||
await run(context)
|
||||
const purchaseWrites = context.calls.filter((call) => call.path.includes('/overseas-procurements') && call.method !== 'GET')
|
||||
assert.deepEqual(purchaseWrites.map((call) => call.method), ['POST', 'PATCH', 'POST'])
|
||||
assert.match(purchaseWrites[1].body.productSpecification, new RegExp(context.marker))
|
||||
assert.equal(context.createdResources.phase4OverseasBusiness.purchase.marker, context.marker)
|
||||
assert.equal(context.createdResources.phase4OverseasBusiness.purchase.finalStatus, 'REJECTED')
|
||||
assert.equal(context.createdResources.phase4OverseasBusiness.purchase.retained, false)
|
||||
const conflictSteps = context.steps.filter((step) => /A0409|异载荷|逆向/.test(step.name))
|
||||
assert.ok(conflictSteps.length >= 7)
|
||||
assert.ok(conflictSteps.every((step) => step.passed))
|
||||
assert.ok(conflictSteps.every((step) => step.requestId && step.traceId))
|
||||
assert.ok(conflictSteps.filter((step) => step.name.includes('旧 version')).every((step) => step.result.body.code === 'A0409'))
|
||||
assert.ok(context.steps.filter((step) => /流水/.test(step.name)).every((step) => step.passed))
|
||||
})
|
||||
|
||||
test('contract locks real candidates, idempotency, version conflicts and trace evidence', () => {
|
||||
for (const token of ['/api/platform/v1/iam/tenants?page=1&size=200&status=ACTIVE', '/overseas-procurements/customer-options?limit=200',
|
||||
'/overseas-procurements/warehouse-options', '/api/merchant/v1/last-leg/overseas-procurements',
|
||||
"decision: 'REJECT'", 'idempotencyKey', 'phase4-truck', 'phase4-looseboard',
|
||||
'旧 version', '/status', "status: 'CANCELLED'", 'finally', 'requestId=', 'traceId=', 'createdResources.phase4OverseasBusiness']) assert.ok(source.includes(token), token)
|
||||
test('missing merchant fixture is an explicit deployment blocker and never a coverage gap', async () => {
|
||||
const context = fixture({ credentials: false })
|
||||
await run(context)
|
||||
assert.equal(context.gaps.length, 0)
|
||||
assert.ok(context.blockers.some((step) => step.name === '采购客户登录前置完整'))
|
||||
assert.equal(context.calls.some((call) => call.path.includes('/overseas-procurements')), false)
|
||||
assert.equal(context.calls.some((call) => call.path.includes('/us-truck-orders')), false)
|
||||
assert.equal(context.calls.some((call) => call.path.includes('/local-looseboard-orders')), false)
|
||||
})
|
||||
|
||||
test('fixture customer must itself be active and cannot borrow another customer identity', async () => {
|
||||
const context = fixture({ activeCustomers: [{ id: '10002', tenantName: '其他客户', status: 'ACTIVE' }] })
|
||||
await run(context)
|
||||
assert.equal(context.gaps.length, 0)
|
||||
assert.ok(context.blockers.some((step) => step.name === '本轮资格客户属于 ACTIVE 候选'))
|
||||
assert.equal(context.calls.some((call) => call.path === '/api/auth/login'), false)
|
||||
assert.equal(context.calls.some((call) => call.path.includes('/overseas-procurements')), false)
|
||||
})
|
||||
|
||||
test('candidate blocker still rejects partial procurement and retires partner warehouse', async () => {
|
||||
const context = fixture({ warehouseOptions: [] })
|
||||
await run(context)
|
||||
assert.equal(context.gaps.length, 0)
|
||||
assert.ok(context.blockers.some((step) => step.name === '采购仓候选存在 ACTIVE 仓'))
|
||||
const registry = context.createdResources.phase4OverseasBusiness
|
||||
assert.equal(registry.purchase.finalStatus, 'REJECTED')
|
||||
assert.equal(registry.purchase.retained, false)
|
||||
assert.equal(registry.partnerWarehouse.finalStatus, 'INACTIVE')
|
||||
assert.equal(registry.partnerWarehouse.retained, false)
|
||||
assert.equal(context.calls.some((call) => call.path.includes('/us-truck-orders')), false)
|
||||
assert.equal(context.calls.some((call) => call.path.includes('/local-looseboard-orders')), false)
|
||||
})
|
||||
|
||||
test('contract locks candidates three-state idempotency histories legal terminals and trace evidence', () => {
|
||||
for (const token of ['/api/platform/v1/iam/tenants?page=1&size=200&status=ACTIVE',
|
||||
'/overseas-procurements/customer-options?limit=200', '/overseas-procurements/warehouse-options',
|
||||
'/api/merchant/v1/last-leg/overseas-procurements', '显式 null 与空串清空', '字段缺失不修改',
|
||||
'同幂等键同载荷精确重放', '同幂等键异载荷返回冲突', '旧 version 返回 A0409',
|
||||
'状态正向推进', '状态逆向回退返回冲突', '流水覆盖', "status: 'CANCELLED'",
|
||||
'requestId=', 'traceId=', 'createdResources.phase4OverseasBusiness']) assert.ok(source.includes(token), token)
|
||||
})
|
||||
|
||||
@@ -9,60 +9,48 @@ function trace(response) { return `HTTP=${response?.http ?? 'missing'}, code=${c
|
||||
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 detailValue(data, field) { return data && Object.hasOwn(data, field) ? data[field] : summary(data)?.[field] }
|
||||
function conflict(response) { return response?.http === 409 || (response?.http === 200 && codeOf(response).endsWith('409')) }
|
||||
function versionConflict(response) { return response?.http === 200 && codeOf(response) === 'A0409' }
|
||||
function longId(value) { return /^[1-9]\d*$/.test(id(value)) }
|
||||
function nonEmpty(value) { return typeof value === 'string' && value.trim().length > 0 }
|
||||
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 }
|
||||
|
||||
function blocker(context, name, response, reason) {
|
||||
context.addStep(MODULE, name, response || null, false, `${reason}; ${trace(response)}`, OPTIONS)
|
||||
return null
|
||||
}
|
||||
|
||||
function expectedConflict(context, name, response, exactVersion = false) {
|
||||
const passed = exactVersion ? versionConflict(response) : conflict(response)
|
||||
context.addStep(MODULE, name, response, passed, trace(response), OPTIONS)
|
||||
return passed
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -78,24 +66,19 @@ function partnerWarehousePayload(context) {
|
||||
}
|
||||
|
||||
async function createPartnerWarehouse(context) {
|
||||
const payload = partnerWarehousePayload(context)
|
||||
const created = await domestic(context, 'POST', '创建本轮采购合作仓', '/api/platform/v1/warehouse-admin', payload)
|
||||
const created = await domestic(context, 'POST', '创建本轮采购合作仓', '/api/platform/v1/warehouse-admin', partnerWarehousePayload(context))
|
||||
const warehouseId = id(created.data)
|
||||
context.assert(MODULE, '采购合作仓创建返回长ID', created.response, longId(warehouseId),
|
||||
const valid = longId(warehouseId)
|
||||
context.assert(MODULE, '采购合作仓创建返回长ID', created.response, valid,
|
||||
`warehouseId=${warehouseId || 'missing'}; ${trace(created.response)}`, OPTIONS)
|
||||
return warehouseId ? { id: warehouseId, finalStatus: 'ACTIVE' } : null
|
||||
if (!valid) return null
|
||||
const resource = { marker: context.marker, id: warehouseId, finalStatus: 'ACTIVE', retained: true }
|
||||
ownedRegistry(context).partnerWarehouse = resource
|
||||
return resource
|
||||
}
|
||||
|
||||
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 = {
|
||||
function procurementPayload(context, activeCustomer) {
|
||||
return {
|
||||
customerName: activeCustomer?.tenantName || activeCustomer?.name || `Customer-${id(activeCustomer?.id)}`,
|
||||
purchaseCategory: 'OFFICE_SUPPLIES', productName: `${context.marker} 采购商品`,
|
||||
productSpecification: 'Phase4 acceptance', productUrl: null, purchaseQuantity: 10,
|
||||
@@ -103,59 +86,186 @@ async function createMerchantProcurement(context, activeCustomer) {
|
||||
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 }
|
||||
async function loginMerchant(context, activeCustomer) {
|
||||
const fixture = context.context?.merchantQualificationFixture
|
||||
if (!nonEmpty(fixture?.username) || !nonEmpty(fixture?.password)) {
|
||||
return blocker(context, '采购客户登录前置完整', null,
|
||||
`缺少 marker=${context.marker} 的客户账号凭据,禁止使用历史客户或降级为 coverageGap`)
|
||||
}
|
||||
const login = await context.request('POST', '/api/auth/login', { username: fixture.username, password: fixture.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)
|
||||
return nonEmpty(overseasToken) ? overseasToken : null
|
||||
}
|
||||
|
||||
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 createMerchantProcurement(context, activeCustomer, overseasToken) {
|
||||
const path = '/api/merchant/v1/last-leg/overseas-procurements'
|
||||
const payload = procurementPayload(context, activeCustomer)
|
||||
const response = await context.requestOverseas('POST', path, payload, {}, overseasToken)
|
||||
const detail = context.apiSuccess(MODULE, '创建本轮客户采购订单', response, trace(response), OPTIONS)
|
||||
const value = summary(detail)
|
||||
const resource = nonEmpty(value?.orderNo) && longId(value?.customerId)
|
||||
? { marker: context.marker, orderNo: id(value.orderNo), customerId: id(value.customerId),
|
||||
version: Number(value.version), finalStatus: value.status, retained: true }
|
||||
: null
|
||||
if (resource) ownedRegistry(context).purchase = resource
|
||||
const valid = nonEmpty(value?.orderNo) && id(value?.customerId) === id(activeCustomer?.id)
|
||||
&& value?.status === 'PENDING_REVIEW' && Number(value?.version) === 0
|
||||
context.assert(MODULE, '客户采购订单创建为待审核且归属正确', response, valid,
|
||||
`orderNo=${id(value?.orderNo) || 'missing'}, customerId=${id(value?.customerId) || 'missing'}, status=${value?.status || 'missing'}, version=${value?.version ?? 'missing'}; ${trace(response)}`, OPTIONS)
|
||||
if (!valid) return null
|
||||
|
||||
const replayResponse = await context.requestOverseas('POST', path, payload, {}, overseasToken)
|
||||
const replayDetail = context.apiSuccess(MODULE, '采购创建同幂等键同载荷精确重放', replayResponse, trace(replayResponse), OPTIONS)
|
||||
const replay = summary(replayDetail)
|
||||
context.assert(MODULE, '采购幂等重放复用订单且版本不漂移', replayResponse,
|
||||
replay?.orderNo === value.orderNo && Number(replay?.version) === Number(value.version),
|
||||
`orderNo=${id(replay?.orderNo) || 'missing'}, version=${replay?.version ?? 'missing'}; ${trace(replayResponse)}`, OPTIONS)
|
||||
|
||||
const mismatch = await context.requestOverseas('POST', path,
|
||||
{ ...payload, productName: `${payload.productName}-DIFFERENT` }, {}, overseasToken)
|
||||
expectedConflict(context, '采购创建同幂等键异载荷返回冲突', mismatch)
|
||||
|
||||
return { resource, payload }
|
||||
}
|
||||
|
||||
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) return blocker(context, '海外业务写验收 ACTIVE 客户前置', tenants.response,
|
||||
'IAM 未返回真实 ACTIVE 客户,禁止跳过完整生命周期')
|
||||
const fixtureCustomerId = id(context.createdResources?.merchantQualificationFixture?.tenantId)
|
||||
const activeCustomer = activeCustomers.find((item) => id(item?.id) === fixtureCustomerId)
|
||||
if (!longId(fixtureCustomerId) || !activeCustomer) return blocker(context, '本轮资格客户属于 ACTIVE 候选', tenants.response,
|
||||
`marker fixture 客户 ${fixtureCustomerId || 'missing'} 不在 ACTIVE 候选中,禁止借用其他客户身份`)
|
||||
const overseasToken = await loginMerchant(context, activeCustomer)
|
||||
if (!overseasToken) return null
|
||||
const partnerWarehouse = await createPartnerWarehouse(context)
|
||||
if (!partnerWarehouse) return null
|
||||
const purchase = await createMerchantProcurement(context, activeCustomer, overseasToken)
|
||||
if (!purchase) return null
|
||||
|
||||
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) && nonEmpty(item?.customerName)),
|
||||
`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) && nonEmpty(item?.warehouseName)),
|
||||
`options=${warehouseOptions.data?.length ?? 'missing'}; ${trace(warehouseOptions.response)}`, OPTIONS)
|
||||
const procurementCustomer = Array.isArray(customerOptions.data)
|
||||
? customerOptions.data.find((item) => id(item?.customerId) === id(activeCustomer.id)) : null
|
||||
const warehouse = Array.isArray(warehouseOptions.data)
|
||||
? warehouseOptions.data.find((item) => id(item?.warehouseId) === partnerWarehouse.id)
|
||||
|| warehouseOptions.data.find((item) => longId(item?.warehouseId)) : null
|
||||
if (!procurementCustomer) return blocker(context, '采购客户候选包含本轮客户', customerOptions.response,
|
||||
`本轮客户 ${id(activeCustomer.id)} 未进入采购客户候选,禁止继续写验收`)
|
||||
if (!warehouse) return blocker(context, '采购仓候选存在 ACTIVE 仓', warehouseOptions.response,
|
||||
'采购仓候选为空,禁止跳过采购三态清空')
|
||||
return { activeCustomer, procurementCustomer, warehouse, customerOptions, warehouseOptions,
|
||||
partnerWarehouse, purchase, overseasToken }
|
||||
}
|
||||
|
||||
function assertProcurementHistory(context, result) {
|
||||
const histories = Array.isArray(result.data?.histories) ? result.data.histories : []
|
||||
const operations = new Set(histories.map((item) => item?.operationType))
|
||||
context.assert(MODULE, '采购流水覆盖创建编辑与审核', result.response,
|
||||
['CREATE', 'UPDATE', 'REVIEW'].every((type) => operations.has(type))
|
||||
&& histories.some((item) => String(item?.remark || item?.changeDetail || '').includes(context.marker)),
|
||||
`historyCount=${histories.length}, operations=${[...operations].join(',') || 'missing'}; ${trace(result.response)}`, OPTIONS)
|
||||
}
|
||||
|
||||
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 }))
|
||||
const owned = prerequisite.purchase.resource
|
||||
const list = await overseas(context, 'GET', '按本轮客户查询采购订单', query(`${BASE}/overseas-procurements`,
|
||||
{ page: 1, size: 20, customerId, orderNo: owned.orderNo }))
|
||||
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' }
|
||||
const row = records(list.data).find((item) => item?.orderNo === owned.orderNo && id(item?.customerId) === customerId)
|
||||
context.assert(MODULE, '采购列表包含本轮 marker 订单', list.response, Boolean(row),
|
||||
`orderNo=${owned.orderNo}, customerId=${customerId}; ${trace(list.response)}`, OPTIONS)
|
||||
if (!row) return blocker(context, '采购列表详情前置完整', list.response,
|
||||
`列表缺少 marker=${context.marker} 的本轮采购订单`)
|
||||
|
||||
const path = query(`${BASE}/overseas-procurements/${encodeURIComponent(owned.orderNo)}`, { customerId })
|
||||
const detail = await overseas(context, 'GET', '查询本轮采购订单详情', path)
|
||||
const before = summary(detail.data)
|
||||
context.assert(MODULE, '采购详情归属和初始版本正确', detail.response,
|
||||
before?.orderNo === owned.orderNo && id(before?.customerId) === customerId && Number(before?.version) === 0,
|
||||
`orderNo=${id(before?.orderNo) || 'missing'}, version=${before?.version ?? 'missing'}; ${trace(detail.response)}`, OPTIONS)
|
||||
|
||||
const setResult = await overseas(context, 'PATCH', '采购可空字段设置非空值', path,
|
||||
{ version: Number(before?.version), productSpecification: `${context.marker}-SPEC`,
|
||||
productUrl: `phase4-product-${context.marker}`, purchaseWarehouseId: warehouseId,
|
||||
purchaseWarehouseName: prerequisite.warehouse.warehouseName, domesticCarrier: `${context.marker}-CARRIER`,
|
||||
domesticTrackingNo: `${context.marker}-TRACK`, estimatedArrivalAt: '2026-12-31T12:00:00', remark: context.marker })
|
||||
const setSummary = summary(setResult.data)
|
||||
context.assert(MODULE, '采购三态第一态设置值并递增版本', setResult.response,
|
||||
setSummary?.productSpecification === `${context.marker}-SPEC`
|
||||
&& detailValue(setResult.data, 'productUrl')?.includes(context.marker)
|
||||
&& id(setSummary?.purchaseWarehouseId) === warehouseId
|
||||
&& Number(setSummary?.version) === Number(before?.version) + 1,
|
||||
`version=${setSummary?.version ?? 'missing'}, warehouseId=${id(setSummary?.purchaseWarehouseId) || 'missing'}; ${trace(setResult.response)}`, OPTIONS)
|
||||
|
||||
const omittedResult = await overseas(context, 'PATCH', '采购可空字段缺失保持原值', path,
|
||||
{ version: Number(setSummary?.version), domesticCarrier: `${context.marker}-CARRIER-2` })
|
||||
const omittedSummary = summary(omittedResult.data)
|
||||
context.assert(MODULE, '采购三态第二态字段缺失不修改', omittedResult.response,
|
||||
omittedSummary?.productSpecification === setSummary?.productSpecification
|
||||
&& detailValue(omittedResult.data, 'productUrl') === detailValue(setResult.data, 'productUrl')
|
||||
&& id(omittedSummary?.purchaseWarehouseId) === warehouseId
|
||||
&& Number(omittedSummary?.version) === Number(setSummary?.version) + 1,
|
||||
`version=${omittedSummary?.version ?? 'missing'}, specification=${omittedSummary?.productSpecification || 'missing'}; ${trace(omittedResult.response)}`, OPTIONS)
|
||||
|
||||
const clearedResult = await overseas(context, 'PATCH', '采购可空字段显式 null 与空串清空', path,
|
||||
{ version: Number(omittedSummary?.version), productSpecification: null, productUrl: '', purchaseWarehouseId: null,
|
||||
purchaseWarehouseName: null, domesticCarrier: null, domesticTrackingNo: '', estimatedArrivalAt: null, remark: null })
|
||||
const clearedSummary = summary(clearedResult.data)
|
||||
context.assert(MODULE, '采购三态第三态显式清空全部生效', clearedResult.response,
|
||||
clearedSummary?.productSpecification == null && detailValue(clearedResult.data, 'productUrl') == null
|
||||
&& clearedSummary?.purchaseWarehouseId == null && clearedSummary?.purchaseWarehouseName == null
|
||||
&& detailValue(clearedResult.data, 'domesticCarrier') == null
|
||||
&& detailValue(clearedResult.data, 'domesticTrackingNo') == null
|
||||
&& detailValue(clearedResult.data, 'estimatedArrivalAt') == null
|
||||
&& Number(clearedSummary?.version) === Number(omittedSummary?.version) + 1,
|
||||
`version=${clearedSummary?.version ?? 'missing'}, specification=${clearedSummary?.productSpecification ?? 'null'}, warehouseId=${clearedSummary?.purchaseWarehouseId ?? 'null'}; ${trace(clearedResult.response)}`, OPTIONS)
|
||||
|
||||
const stale = await context.requestOverseas('PATCH', path,
|
||||
{ version: Number(setSummary?.version), productSpecification: 'STALE-DO-NOT-APPLY' })
|
||||
expectedConflict(context, '采购旧 version 返回 A0409', stale, true)
|
||||
const afterConflict = await overseas(context, 'GET', '采购冲突后零漂移回查', path)
|
||||
const afterConflictSummary = summary(afterConflict.data)
|
||||
context.assert(MODULE, '采购旧 version 冲突未造成部分更新', afterConflict.response,
|
||||
afterConflictSummary?.productSpecification == null
|
||||
&& Number(afterConflictSummary?.version) === Number(clearedSummary?.version),
|
||||
`version=${afterConflictSummary?.version ?? 'missing'}, specification=${afterConflictSummary?.productSpecification ?? 'null'}; ${trace(afterConflict.response)}`, OPTIONS)
|
||||
|
||||
const rejected = await overseas(context, 'POST', '驳回本轮采购订单并进入合法终态',
|
||||
query(`${BASE}/overseas-procurements/${encodeURIComponent(owned.orderNo)}/review`, { customerId }),
|
||||
{ decision: 'REJECT', remark: `${context.marker}-REJECTED` })
|
||||
const rejectedSummary = summary(rejected.data)
|
||||
context.assert(MODULE, '采购审核正向驳回成功', rejected.response,
|
||||
rejectedSummary?.status === 'REJECTED' && Number(rejectedSummary?.version) === Number(clearedSummary?.version) + 1,
|
||||
`status=${rejectedSummary?.status || 'missing'}, version=${rejectedSummary?.version ?? 'missing'}; ${trace(rejected.response)}`, OPTIONS)
|
||||
const reverseReview = await context.requestOverseas('POST',
|
||||
query(`${BASE}/overseas-procurements/${encodeURIComponent(owned.orderNo)}/review`, { customerId }),
|
||||
{ decision: 'APPROVE', remark: 'REVERSE-DO-NOT-APPLY' })
|
||||
expectedConflict(context, '采购审核终态逆向返回冲突', reverseReview)
|
||||
|
||||
const final = await overseas(context, 'GET', '采购最终状态与流水回查', path)
|
||||
const finalSummary = summary(final.data)
|
||||
context.assert(MODULE, '采购 marker 资源最终保持合法终态', final.response,
|
||||
finalSummary?.status === 'REJECTED' && Number(finalSummary?.version) === Number(rejectedSummary?.version),
|
||||
`status=${finalSummary?.status || 'missing'}, version=${finalSummary?.version ?? 'missing'}; ${trace(final.response)}`, OPTIONS)
|
||||
assertProcurementHistory(context, final)
|
||||
Object.assign(owned, { version: Number(finalSummary?.version), finalStatus: finalSummary?.status,
|
||||
retained: finalSummary?.status !== 'REJECTED' })
|
||||
}
|
||||
|
||||
function truckPayload(context) {
|
||||
@@ -170,72 +280,6 @@ function truckPayload(context) {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -248,56 +292,240 @@ function looseboardPayload(context) {
|
||||
}
|
||||
}
|
||||
|
||||
async function assertCreateIdempotency(context, base, payload, customerId, label) {
|
||||
const path = query(base, { customerId })
|
||||
const created = await overseas(context, 'POST', `创建 marker ${label}订单`, path, payload)
|
||||
const value = summary(created.data)
|
||||
if (!nonEmpty(value?.orderNo)) return { created, value, path }
|
||||
const replay = await overseas(context, 'POST', `${label}创建同幂等键同载荷精确重放`, path, payload)
|
||||
const replayValue = summary(replay.data)
|
||||
context.assert(MODULE, `${label}幂等重放复用订单且版本不漂移`, replay.response,
|
||||
replayValue?.orderNo === value.orderNo && Number(replayValue?.version) === Number(value.version),
|
||||
`orderNo=${id(replayValue?.orderNo) || 'missing'}, version=${replayValue?.version ?? 'missing'}; ${trace(replay.response)}`, OPTIONS)
|
||||
const mismatch = await context.requestOverseas('POST', path,
|
||||
{ ...payload, destination: `${payload.destination}-DIFFERENT` })
|
||||
expectedConflict(context, `${label}创建同幂等键异载荷返回冲突`, mismatch)
|
||||
return { created, value, path }
|
||||
}
|
||||
|
||||
async function cancelMarkerOrder(context, resource, base, cancellableStatuses, label) {
|
||||
if (!resource || resource.marker !== context.marker || !resource.orderNo || !longId(resource.customerId)) {
|
||||
blocker(context, `${label}最终处置资源归属完整`, null,
|
||||
`缺少 marker=${context.marker} 的 ${label} 资源登记,无法证明合法终态`)
|
||||
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 (!cancellableStatuses.includes(currentSummary?.status)) {
|
||||
resource.finalStatus = currentSummary?.status || resource.finalStatus; resource.version = Number(currentSummary?.version); resource.retained = true
|
||||
blocker(context, `${label}测试数据最终处置`, current.response,
|
||||
`本轮 marker 资源 ${resource.orderNo} 当前状态 ${resource.finalStatus || 'missing'} 不允许安全取消`)
|
||||
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
|
||||
}
|
||||
|
||||
async function truckFlow(context, customerId) {
|
||||
const base = `${BASE}/us-truck-orders`
|
||||
const payload = truckPayload(context)
|
||||
const creation = await assertCreateIdempotency(context, base, payload, customerId, '卡车')
|
||||
const created = creation.created; const createdSummary = creation.value
|
||||
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 blocker(context, '卡车完整生命周期创建前置', created.response, '卡车创建未返回订单号')
|
||||
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 list = await overseas(context, 'GET', '按本轮客户与单号查询卡车列表', query(base,
|
||||
{ page: 1, size: 20, customerId, orderNo }))
|
||||
assertPage(context, '本土卡车订单', list)
|
||||
context.assert(MODULE, '卡车列表包含本轮 marker 订单', list.response,
|
||||
records(list.data).some((item) => item?.orderNo === orderNo && id(item?.customerId) === customerId),
|
||||
`orderNo=${orderNo}; ${trace(list.response)}`, OPTIONS)
|
||||
const detail = await overseas(context, 'GET', '查询 marker 本土卡车详情', path)
|
||||
const before = summary(detail.data)
|
||||
const edited = await overseas(context, 'PATCH', '编辑 marker 本土卡车订单', path,
|
||||
{ version: Number(before?.version), destination: `${payload.destination}-EDIT`, remark: `${context.marker}-truck-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_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' })
|
||||
expectedConflict(context, '卡车旧 version 编辑返回 A0409', stale, true)
|
||||
const afterStale = await overseas(context, 'GET', '卡车旧 version 冲突后零漂移回查', path)
|
||||
const afterStaleSummary = summary(afterStale.data)
|
||||
context.assert(MODULE, '卡车冲突未造成部分更新', afterStale.response,
|
||||
afterStaleSummary?.destination === after?.destination && Number(afterStaleSummary?.version) === Number(after?.version),
|
||||
`destination=${afterStaleSummary?.destination || 'missing'}, version=${afterStaleSummary?.version ?? 'missing'}; ${trace(afterStale.response)}`, OPTIONS)
|
||||
|
||||
const statusPath = query(`${base}/${encodeURIComponent(orderNo)}/status`, { customerId })
|
||||
const forwarded = await overseas(context, 'PATCH', '卡车状态正向推进到待入仓', statusPath,
|
||||
{ version: Number(afterStaleSummary?.version), status: 'WAITING_INBOUND', remark: `${context.marker}-FORWARD` })
|
||||
const forwardedSummary = summary(forwarded.data)
|
||||
context.assert(MODULE, '卡车状态正向推进成功', forwarded.response,
|
||||
forwardedSummary?.status === 'WAITING_INBOUND'
|
||||
&& Number(forwardedSummary?.version) === Number(afterStaleSummary?.version) + 1,
|
||||
`status=${forwardedSummary?.status || 'missing'}, version=${forwardedSummary?.version ?? 'missing'}; ${trace(forwarded.response)}`, OPTIONS)
|
||||
const reversed = await context.requestOverseas('PATCH', statusPath,
|
||||
{ version: Number(forwardedSummary?.version), status: 'PENDING_REVIEW', remark: 'REVERSE-DO-NOT-APPLY' })
|
||||
expectedConflict(context, '卡车状态逆向回退返回冲突', reversed)
|
||||
const history = await overseas(context, 'GET', '卡车状态冲突后流水回查', path)
|
||||
const historySummary = summary(history.data)
|
||||
const histories = Array.isArray(history.data?.histories) ? history.data.histories : []
|
||||
context.assert(MODULE, '卡车流水覆盖创建编辑与正向状态', history.response,
|
||||
historySummary?.status === 'WAITING_INBOUND' && histories.length >= 3
|
||||
&& histories.some((item) => String(item?.remark || '').includes(context.marker)),
|
||||
`status=${historySummary?.status || 'missing'}, historyCount=${histories.length}; ${trace(history.response)}`, OPTIONS)
|
||||
Object.assign(registry.truck, { version: Number(historySummary?.version), finalStatus: historySummary?.status })
|
||||
} finally {
|
||||
await cancelMarkerOrder(context, registry.truck, base, ['PENDING_REVIEW', 'WAITING_INBOUND'], '卡车')
|
||||
}
|
||||
}
|
||||
|
||||
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 creation = await assertCreateIdempotency(context, base, payload, customerId, '散板')
|
||||
const created = creation.created; const createdSummary = creation.value
|
||||
const orderNo = id(createdSummary?.orderNo)
|
||||
context.assert(MODULE, '散板创建返回本轮业务键和待接单状态', created.response,
|
||||
Boolean(orderNo) && id(createdSummary?.customerId) === customerId && createdSummary?.status === 'PENDING_ACCEPTANCE' && Number(createdSummary?.version) === 0,
|
||||
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
|
||||
if (!orderNo) return blocker(context, '散板完整生命周期创建前置', created.response, '散板创建未返回订单号')
|
||||
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 list = await overseas(context, 'GET', '按本轮客户与单号查询散板列表', query(base,
|
||||
{ page: 1, size: 20, customerId, orderNo }))
|
||||
assertPage(context, '本土散板订单', list)
|
||||
context.assert(MODULE, '散板列表包含本轮 marker 订单', list.response,
|
||||
records(list.data).some((item) => item?.orderNo === orderNo && id(item?.customerId) === customerId),
|
||||
`orderNo=${orderNo}; ${trace(list.response)}`, OPTIONS)
|
||||
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` })
|
||||
{ ...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',
|
||||
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
|
||||
expectedConflict(context, '散板旧 version 编辑返回 A0409', stale, true)
|
||||
const afterStale = await overseas(context, 'GET', '散板旧 version 冲突后零漂移回查', path)
|
||||
const afterStaleSummary = summary(afterStale.data)
|
||||
context.assert(MODULE, '散板冲突未造成部分更新', afterStale.response,
|
||||
afterStaleSummary?.destination === after?.destination && Number(afterStaleSummary?.version) === Number(after?.version),
|
||||
`destination=${afterStaleSummary?.destination || 'missing'}, version=${afterStaleSummary?.version ?? 'missing'}; ${trace(afterStale.response)}`, OPTIONS)
|
||||
|
||||
const statusPath = query(`${base}/${encodeURIComponent(orderNo)}/status`, { customerId })
|
||||
const forwarded = await overseas(context, 'PATCH', '散板状态正向推进到已派车', statusPath,
|
||||
{ version: Number(afterStaleSummary?.version), status: 'DISPATCHED', remark: `${context.marker}-FORWARD` })
|
||||
const forwardedSummary = summary(forwarded.data)
|
||||
context.assert(MODULE, '散板状态正向推进成功', forwarded.response,
|
||||
forwardedSummary?.status === 'DISPATCHED'
|
||||
&& Number(forwardedSummary?.version) === Number(afterStaleSummary?.version) + 1,
|
||||
`status=${forwardedSummary?.status || 'missing'}, version=${forwardedSummary?.version ?? 'missing'}; ${trace(forwarded.response)}`, OPTIONS)
|
||||
const reversed = await context.requestOverseas('PATCH', statusPath,
|
||||
{ version: Number(forwardedSummary?.version), status: 'PENDING_ACCEPTANCE', remark: 'REVERSE-DO-NOT-APPLY' })
|
||||
expectedConflict(context, '散板状态逆向回退返回冲突', reversed)
|
||||
const history = await overseas(context, 'GET', '散板状态冲突后流水回查', path)
|
||||
const historySummary = summary(history.data)
|
||||
const histories = Array.isArray(history.data?.histories) ? history.data.histories : []
|
||||
const operations = new Set(histories.map((item) => item?.operationType))
|
||||
context.assert(MODULE, '散板流水覆盖创建编辑与正向状态', history.response,
|
||||
historySummary?.status === 'DISPATCHED' && ['CREATE', 'UPDATE', 'STATUS'].every((type) => operations.has(type))
|
||||
&& histories.some((item) => String(item?.remark || item?.changeDetail || '').includes(context.marker)),
|
||||
`status=${historySummary?.status || 'missing'}, historyCount=${histories.length}, operations=${[...operations].join(',') || 'missing'}; ${trace(history.response)}`, OPTIONS)
|
||||
Object.assign(registry.looseboard, { version: Number(historySummary?.version), finalStatus: historySummary?.status })
|
||||
} finally {
|
||||
await cancelMarkerOrder(context, registry.looseboard, base, 'PENDING_ACCEPTANCE', '散板')
|
||||
await cancelMarkerOrder(context, registry.looseboard, base, ['PENDING_ACCEPTANCE', 'DISPATCHED'], '散板')
|
||||
}
|
||||
}
|
||||
|
||||
async function retirePartnerWarehouse(context, resource) {
|
||||
if (!resource?.id) return
|
||||
const retired = await context.request('PATCH',
|
||||
`/api/platform/v1/warehouse-admin/${encodeURIComponent(resource.id)}/status?tenantId=0&status=INACTIVE`)
|
||||
const passed = retired?.http === 200 && codeOf(retired) === '200'
|
||||
context.addStep(MODULE, '本轮采购合作仓已停用收口', retired, passed, trace(retired), OPTIONS)
|
||||
resource.finalStatus = passed ? 'INACTIVE' : resource.finalStatus
|
||||
resource.retained = !passed
|
||||
}
|
||||
|
||||
async function finalizePendingPurchase(context, resource) {
|
||||
if (!resource?.orderNo || !longId(resource?.customerId)
|
||||
|| ['REJECTED', 'APPROVED'].includes(resource.finalStatus)) return
|
||||
const path = query(`${BASE}/overseas-procurements/${encodeURIComponent(resource.orderNo)}`,
|
||||
{ customerId: resource.customerId })
|
||||
const current = await overseas(context, 'GET', '采购最终处置前回查本轮订单', path)
|
||||
const currentSummary = summary(current.data)
|
||||
if (['REJECTED', 'APPROVED'].includes(currentSummary?.status)) {
|
||||
resource.finalStatus = currentSummary.status; resource.version = Number(currentSummary.version); resource.retained = false
|
||||
return
|
||||
}
|
||||
if (currentSummary?.status !== 'PENDING_REVIEW') {
|
||||
blocker(context, '采购测试数据最终处置', current.response,
|
||||
`本轮 marker 资源 ${resource.orderNo} 当前状态 ${currentSummary?.status || 'missing'} 无法安全驳回`)
|
||||
resource.finalStatus = currentSummary?.status || resource.finalStatus; resource.retained = true
|
||||
return
|
||||
}
|
||||
const rejected = await overseas(context, 'POST', '驳回未完成的本轮采购订单',
|
||||
query(`${BASE}/overseas-procurements/${encodeURIComponent(resource.orderNo)}/review`,
|
||||
{ customerId: resource.customerId }),
|
||||
{ decision: 'REJECT', remark: `${context.marker}-prerequisite-cleanup` })
|
||||
const rejectedSummary = summary(rejected.data)
|
||||
const verified = await overseas(context, 'GET', '回查未完成采购订单最终状态', path)
|
||||
const finalSummary = summary(verified.data)
|
||||
const passed = rejectedSummary?.status === 'REJECTED' && finalSummary?.status === 'REJECTED'
|
||||
&& Number(finalSummary?.version) === Number(rejectedSummary?.version)
|
||||
context.assert(MODULE, '采购本轮测试数据最终合法驳回', verified.response, passed,
|
||||
`orderNo=${resource.orderNo}, status=${finalSummary?.status || 'missing'}, version=${finalSummary?.version ?? 'missing'}; review=${trace(rejected.response)}; verify=${trace(verified.response)}`, OPTIONS)
|
||||
resource.finalStatus = finalSummary?.status || rejectedSummary?.status || resource.finalStatus
|
||||
resource.version = Number(finalSummary?.version ?? rejectedSummary?.version)
|
||||
resource.retained = !passed
|
||||
}
|
||||
|
||||
export async function run(context) {
|
||||
const prerequisite = await prerequisites(context)
|
||||
if (!prerequisite) return
|
||||
let prerequisite = null
|
||||
try {
|
||||
prerequisite = await prerequisites(context)
|
||||
if (!prerequisite) return
|
||||
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)
|
||||
}
|
||||
const registry = context.createdResources.phase4OverseasBusiness
|
||||
await finalizePendingPurchase(context, registry?.purchase)
|
||||
await retirePartnerWarehouse(context, registry?.partnerWarehouse)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user