优化
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
import { inspectOceanBookingIdentity, truckAppointmentPayload } from './truck-appointments.mjs'
|
||||
|
||||
test('truck appointment accepts only the current marker ocean booking identity', () => {
|
||||
const current = inspectOceanBookingIdentity('RUN-2', {
|
||||
id: '91', marker: 'RUN-2', customerId: '21', warehouseId: '41', customerName: '本轮客户',
|
||||
})
|
||||
assert.deepEqual(current.identity, {
|
||||
oceanBookingId: '91', customerId: '21', warehouseId: '41', customerName: '本轮客户',
|
||||
})
|
||||
const stale = inspectOceanBookingIdentity('RUN-2', {
|
||||
id: '90', marker: 'RUN-1', customerId: '20', warehouseId: '40', customerName: '历史客户',
|
||||
})
|
||||
assert.equal(stale.identity, null)
|
||||
assert.match(stale.reason, /marker 不属于本轮/)
|
||||
})
|
||||
|
||||
test('truck appointment fills missing identity only from the current booking detail', () => {
|
||||
const partial = { id: '91', marker: 'RUN-2', customerId: '21' }
|
||||
const beforeDetail = inspectOceanBookingIdentity('RUN-2', partial)
|
||||
assert.equal(beforeDetail.identity, null)
|
||||
assert.deepEqual(beforeDetail.missingFields, ['warehouseId', 'customerName'])
|
||||
const resolved = inspectOceanBookingIdentity('RUN-2', partial, { oceanBooking: {
|
||||
id: 91, customerId: 21, warehouseId: 41, customerName: '本轮客户',
|
||||
} })
|
||||
assert.deepEqual(resolved.identity, {
|
||||
oceanBookingId: '91', customerId: '21', warehouseId: '41', customerName: '本轮客户',
|
||||
})
|
||||
})
|
||||
|
||||
test('truck appointment rejects mismatched booking detail instead of mixing identities', () => {
|
||||
const wrongResource = inspectOceanBookingIdentity('RUN-2', { id: '91', marker: 'RUN-2' }, {
|
||||
oceanBooking: { id: '92', customerId: '21', warehouseId: '41', customerName: '其他订舱' },
|
||||
})
|
||||
assert.equal(wrongResource.identity, null)
|
||||
assert.match(wrongResource.reason, /资源归属不一致/)
|
||||
|
||||
const conflictingCustomer = inspectOceanBookingIdentity('RUN-2', {
|
||||
id: '91', marker: 'RUN-2', customerId: '21',
|
||||
}, { oceanBooking: { id: '91', customerId: '22', warehouseId: '41', customerName: '本轮客户' } })
|
||||
assert.equal(conflictingCustomer.identity, null)
|
||||
assert.match(conflictingCustomer.reason, /customerId/)
|
||||
})
|
||||
|
||||
test('create edit and stale replay payloads keep the resolved booking identity stable', () => {
|
||||
const context = { marker: 'RUN-2' }
|
||||
const identity = { oceanBookingId: '91', customerId: '21', warehouseId: '41', customerName: '本轮客户' }
|
||||
const payloads = [
|
||||
truckAppointmentPayload(context, '2', identity),
|
||||
truckAppointmentPayload(context, '2', identity, 3),
|
||||
truckAppointmentPayload(context, '2', identity, 0),
|
||||
]
|
||||
for (const payload of payloads) {
|
||||
assert.deepEqual({
|
||||
oceanBookingId: payload.oceanBookingId, customerId: payload.customerId,
|
||||
warehouseId: payload.warehouseId, customerName: payload.customerName,
|
||||
}, identity)
|
||||
}
|
||||
assert.deepEqual(payloads.map((payload) => payload.expectedVersion), [0, 3, 0])
|
||||
})
|
||||
|
||||
test('truck appointment source has no historical list or hardcoded identity fallback', () => {
|
||||
const source = readFileSync(new URL('truck-appointments.mjs', import.meta.url), 'utf8')
|
||||
assert.doesNotMatch(source, /customerId:\s*['"]10001['"]|warehouseId:\s*['"]193000000000000001['"]/)
|
||||
assert.match(source, /\/api\/platform\/v1\/ocean-bookings/)
|
||||
assert.match(source, /未使用历史订舱或硬编码客户\/仓库/)
|
||||
})
|
||||
@@ -1,6 +1,7 @@
|
||||
const MODULE = 'truck-appointments'
|
||||
const OPTIONS = { scenario: 'truck-appointment-lifecycle', deploymentBlocker: true }
|
||||
const BASE = '/api/platform/v1/truck-appointments'
|
||||
const OCEAN_BOOKING_BASE = '/api/platform/v1/ocean-bookings'
|
||||
|
||||
function id(value) { return value == null ? '' : String(value) }
|
||||
function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) }
|
||||
@@ -36,10 +37,39 @@ async function uploadAttachment(context, appointmentId) {
|
||||
} catch (error) { return { method: 'POST', path, http: 0, body: null, raw: String(error), requestId: null, traceId: null } }
|
||||
}
|
||||
|
||||
function payload(context, suffix, version = 0) {
|
||||
export function inspectOceanBookingIdentity(marker, resource, detail = null) {
|
||||
if (!resource?.id) return { identity: null, reason: '本轮 oceanBooking.id 缺失' }
|
||||
if (resource.marker !== marker) {
|
||||
return { identity: null, reason: `oceanBooking marker 不属于本轮:expected=${marker}, actual=${resource.marker || 'missing'}` }
|
||||
}
|
||||
const booking = detail?.oceanBooking || null
|
||||
if (booking && id(booking.id) !== id(resource.id)) {
|
||||
return { identity: null, reason: `订舱详情资源归属不一致:expected=${id(resource.id)}, actual=${id(booking.id) || 'missing'}` }
|
||||
}
|
||||
const fields = ['customerId', 'warehouseId', 'customerName']
|
||||
for (const field of fields) {
|
||||
const published = id(resource[field])
|
||||
const queried = id(booking?.[field])
|
||||
if (published && queried && published !== queried) {
|
||||
return { identity: null, reason: `订舱身份字段冲突:field=${field}, published=${published}, detail=${queried}` }
|
||||
}
|
||||
}
|
||||
const identity = {
|
||||
oceanBookingId: id(resource.id),
|
||||
customerId: id(resource.customerId || booking?.customerId),
|
||||
warehouseId: id(resource.warehouseId || booking?.warehouseId),
|
||||
customerName: id(resource.customerName || booking?.customerName),
|
||||
}
|
||||
const missingFields = fields.filter((field) => !identity[field])
|
||||
return missingFields.length
|
||||
? { identity: null, reason: `本轮订舱身份字段缺失:${missingFields.join(',')}`, missingFields }
|
||||
: { identity, reason: null, missingFields: [] }
|
||||
}
|
||||
|
||||
export function truckAppointmentPayload(context, suffix, identity, version = 0) {
|
||||
return {
|
||||
outboundOrderId: null, oceanBookingId: context.createdResources?.oceanBooking?.id || null, containerBookingId: null, loadingPlanId: null,
|
||||
warehouseId: '193000000000000001', customerId: '10001', customerName: `拖车验收客户-${suffix}`,
|
||||
outboundOrderId: null, oceanBookingId: identity.oceanBookingId, containerBookingId: null, loadingPlanId: null,
|
||||
warehouseId: identity.warehouseId, customerId: identity.customerId, customerName: identity.customerName,
|
||||
carrierId: null, carrierName: `承运商-${suffix}`, fleetContact: '车队联系人', fleetPhone: '13800000004',
|
||||
driverName: '司机验收', driverPhone: '13800000005', driverIdCard: null, vehicleNo: `粤B-${suffix.slice(-6)}`,
|
||||
storeName: `门店-${suffix}`, storeContact: '门店联系人', storePhone: '13800000006', storeAddress: '深圳市南山区验收地址',
|
||||
@@ -64,13 +94,35 @@ export async function run(context) {
|
||||
const oceanBookings = await import(new URL('./ocean-bookings.mjs', import.meta.url))
|
||||
await oceanBookings.run(context)
|
||||
}
|
||||
if (!context.createdResources?.oceanBooking?.id) {
|
||||
const oceanBooking = context.createdResources?.oceanBooking
|
||||
if (!oceanBooking?.id) {
|
||||
context.addStep(MODULE, '创建拖车预约所需隔离订舱 fixture', null, false,
|
||||
'本轮 ocean-bookings 未返回可关联的隔离订舱,未使用历史订舱', OPTIONS)
|
||||
return
|
||||
}
|
||||
let identityResult = inspectOceanBookingIdentity(context.marker, oceanBooking)
|
||||
if (!identityResult.identity && identityResult.missingFields?.length) {
|
||||
const bookingPath = `${OCEAN_BOOKING_BASE}/${encodeURIComponent(id(oceanBooking.id))}`
|
||||
const bookingResponse = await request(context, 'GET', bookingPath)
|
||||
if (bookingResponse.http !== 200 || codeOf(bookingResponse) !== '200') {
|
||||
context.addStep(MODULE, '读取本轮订舱身份快照', bookingResponse, false,
|
||||
`path=${bookingPath}, HTTP=${bookingResponse.http}, code=${codeOf(bookingResponse)}, requestId=${bookingResponse.requestId || 'missing'}, traceId=${bookingResponse.traceId || 'missing'}`, OPTIONS)
|
||||
return
|
||||
}
|
||||
identityResult = inspectOceanBookingIdentity(context.marker, oceanBooking, data(bookingResponse))
|
||||
context.addStep(MODULE, '读取本轮订舱身份快照', bookingResponse, Boolean(identityResult.identity),
|
||||
identityResult.identity
|
||||
? `oceanBookingId=${identityResult.identity.oceanBookingId}, customerId=${identityResult.identity.customerId}, warehouseId=${identityResult.identity.warehouseId}`
|
||||
: `${identityResult.reason}, path=${bookingPath}, requestId=${bookingResponse.requestId || 'missing'}, traceId=${bookingResponse.traceId || 'missing'}`, OPTIONS)
|
||||
}
|
||||
if (!identityResult.identity) {
|
||||
context.addStep(MODULE, '创建拖车预约所需本轮订舱身份', null, false,
|
||||
`${identityResult.reason};未使用历史订舱或硬编码客户/仓库`, OPTIONS)
|
||||
return
|
||||
}
|
||||
const bookingIdentity = Object.freeze({ ...identityResult.identity })
|
||||
|
||||
const createResponse = await request(context, 'POST', BASE, payload(context, suffix)); const appointment = context.apiSuccess(MODULE, '创建隔离拖车预约', createResponse, '', OPTIONS)
|
||||
const createResponse = await request(context, 'POST', BASE, truckAppointmentPayload(context, suffix, bookingIdentity)); const appointment = context.apiSuccess(MODULE, '创建隔离拖车预约', createResponse, '', OPTIONS)
|
||||
const appointmentId = id(appointment?.id)
|
||||
context.assert(MODULE, '拖车预约费用、司机车辆和地址字段完整', createResponse, Boolean(appointmentId) && appointment?.status === 'DRAFT' && appointment?.payableAmount != null && appointment?.receivableAmount != null, `id=${appointmentId || 'missing'}, status=${appointment?.status || 'missing'}`, OPTIONS)
|
||||
if (!appointmentId) return
|
||||
@@ -87,8 +139,8 @@ export async function run(context) {
|
||||
if (attachmentId) { const removed = await request(context, 'DELETE', `${path}/attachments/${attachmentId}`); context.apiSuccess(MODULE, '删除本轮拖车预约附件引用', removed, `attachmentId=${attachmentId}`, OPTIONS) }
|
||||
|
||||
let version = Number(appointment.version)
|
||||
const edit = await request(context, 'PUT', path, { ...payload(context, suffix, version), vehicleNo: `粤C-${suffix.slice(-6)}` }); const edited = context.apiSuccess(MODULE, '编辑拖车预约', edit, `version=${version}`, OPTIONS); version = Number(edited?.version)
|
||||
const stale = await request(context, 'PUT', path, { ...payload(context, suffix, 0), vehicleNo: `STALE-${suffix}` })
|
||||
const edit = await request(context, 'PUT', path, { ...truckAppointmentPayload(context, suffix, bookingIdentity, version), vehicleNo: `粤C-${suffix.slice(-6)}` }); const edited = context.apiSuccess(MODULE, '编辑拖车预约', edit, `version=${version}`, OPTIONS); version = Number(edited?.version)
|
||||
const stale = await request(context, 'PUT', path, { ...truckAppointmentPayload(context, suffix, bookingIdentity, 0), vehicleNo: `STALE-${suffix}` })
|
||||
context.addStep(MODULE, '拖车预约旧版本编辑返回冲突', stale, stale.http === 200 && ['A0409','409'].includes(codeOf(stale)), `HTTP=${stale.http}, code=${codeOf(stale)}`, OPTIONS)
|
||||
const after = await request(context, 'GET', path); const afterData = data(after); context.apiSuccess(MODULE, '冲突后回查拖车预约', after, '', OPTIONS)
|
||||
context.assert(MODULE, '拖车预约冲突未部分更新', after, afterData?.vehicleNo === edited?.vehicleNo, `vehicleNo=${afterData?.vehicleNo || 'missing'}`, OPTIONS); version = Number(afterData?.version)
|
||||
|
||||
Reference in New Issue
Block a user