From 9e0f41e78b53acc97ec83ebdca71aab5513ce9f6 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 24 Aug 2026 19:45:22 +0800 Subject: [PATCH] test: strengthen phase4 overseas business acceptance --- ...phase4-overseas-business-contract.test.mjs | 292 ++++++--- .../phase4-overseas-business.mjs | 576 ++++++++++++------ 2 files changed, 626 insertions(+), 242 deletions(-) diff --git a/scripts/admin-test-env-modules/phase4-overseas-business-contract.test.mjs b/scripts/admin-test-env-modules/phase4-overseas-business-contract.test.mjs index 3e41dea1b..f7bee94cc 100644 --- a/scripts/admin-test-env-modules/phase4-overseas-business-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase4-overseas-business-contract.test.mjs @@ -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) }) diff --git a/scripts/admin-test-env-modules/phase4-overseas-business.mjs b/scripts/admin-test-env-modules/phase4-overseas-business.mjs index 21fab5d74..5f6075550 100644 --- a/scripts/admin-test-env-modules/phase4-overseas-business.mjs +++ b/scripts/admin-test-env-modules/phase4-overseas-business.mjs @@ -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) } }