This commit is contained in:
2026-09-04 15:19:45 +08:00
parent 2c21d27f61
commit 4aed53a8af
18 changed files with 162 additions and 84 deletions
@@ -1,5 +1,6 @@
import { ensureMatsonCarrierPartner } from './carrier-partner-fixture.mjs'
import { oceanBookingDraft } from './ocean-booking-fixture.mjs'
import { truckAppointmentFieldGaps } from './fulfillment-list-field-contract.mjs'
const MODULE = 'truck-appointments'
const OPTIONS = { scenario: 'booking-level-truck-appointment-lifecycle', deploymentBlocker: true }
@@ -9,6 +10,9 @@ const FORBIDDEN_TRUCK_FIELDS = ['containerBookingId', 'loadingPlanId', 'containe
function id(value) { return value == null ? '' : String(value) }
function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) }
function responseEvidence(response) {
return `HTTP=${response?.http ?? 'missing'}, code=${codeOf(response)}, requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}`
}
function data(response) { return codeOf(response) === '200' ? response.body.data : null }
function failure(response) { return response?.http === 0 || response?.http >= 400 || (response?.http === 200 && codeOf(response) !== '200') }
function records(value) { return Array.isArray(value) ? value : value?.records || [] }
@@ -201,8 +205,10 @@ export async function run(context) {
const appointmentId = id(appointment?.id); if (!appointmentId) return
const path = `${TRUCKS}/${appointmentId}`
context.assert(MODULE, '创建响应完全无柜次协议且费用正确', createdResponse,
!hasForbiddenTruckFields(appointment) && appointment?.currency === 'RMB' && Number(appointment?.payableAmount) === 0,
`appointmentNo=${appointment?.appointmentNo || 'missing'}`, OPTIONS)
truckAppointmentFieldGaps(appointment).length === 0
&& !hasForbiddenTruckFields(appointment) && appointment?.currency === 'RMB'
&& Number(appointment?.payableAmount) === 0,
`appointmentNo=${appointment?.appointmentNo || 'missing'}, fieldGaps=${truckAppointmentFieldGaps(appointment).join('|') || 'none'}`, OPTIONS)
const afterCreateCandidate = await candidate(context, identity)
context.assert(MODULE, '创建后订舱立即退出候选', afterCreateCandidate.response, !afterCreateCandidate.item,
`appointmentNo=${appointment.appointmentNo}`, OPTIONS)
@@ -219,12 +225,45 @@ export async function run(context) {
`dispatch-order-${identity.suffix}.pdf`)
const uploadAttachmentResponse = await context.requestMultipart('POST', `${path}/attachments/upload`,
attachmentForm, context.token)
const attachment = context.apiSuccess(MODULE, '上传DISPATCH_ORDER拖车附件', uploadAttachmentResponse, '', OPTIONS)
const attachment = context.apiSuccess(MODULE, '上传并关联DISPATCH_ORDER拖车附件', uploadAttachmentResponse, '', OPTIONS)
const attachmentListResponse = await request(context, 'GET', `${path}/attachments`)
const attachmentList = context.apiSuccess(MODULE, '查询拖车附件列表', attachmentListResponse, '', OPTIONS)
context.assert(MODULE, '附件列表包含本轮运营附件', attachmentListResponse,
records(attachmentList).some((item) => id(item?.id) === id(attachment?.id) && item?.attachmentType === 'DISPATCH_ORDER'),
`attachmentId=${id(attachment?.id) || 'missing'}`, OPTIONS)
if (!attachment?.id || !attachment?.fileId) return
const deleteUploadedResponse = await request(context, 'DELETE',
`${path}/attachments/${encodeURIComponent(id(attachment?.id))}`)
context.addStep(MODULE, '删除首次上传的拖车附件引用', deleteUploadedResponse,
deleteUploadedResponse.http === 200 && codeOf(deleteUploadedResponse) === '200',
responseEvidence(deleteUploadedResponse), OPTIONS)
const referencedResponse = await request(context, 'POST', `${path}/attachments`, {
fileId: id(attachment?.fileId), attachmentType: 'DISPATCH_ORDER',
fileName: attachment?.fileName || `dispatch-order-${identity.suffix}.pdf`,
fileHash: attachment?.fileHash || null,
})
const referenced = context.apiSuccess(MODULE, '重新引用平台归属拖车附件', referencedResponse, '', OPTIONS)
const referencedListResponse = await request(context, 'GET', `${path}/attachments`)
const referencedList = context.apiSuccess(MODULE, '回查重新引用的拖车附件', referencedListResponse, '', OPTIONS)
context.assert(MODULE, '附件列表包含重新引用的DISPATCH_ORDER', referencedListResponse,
records(referencedList).some((item) => id(item?.id) === id(referenced?.id)
&& id(item?.fileId) === id(attachment?.fileId) && item?.attachmentType === 'DISPATCH_ORDER'),
`attachmentId=${id(referenced?.id) || 'missing'}, fileId=${id(attachment?.fileId) || 'missing'}`, OPTIONS)
const deleteReferencedResponse = await request(context, 'DELETE',
`${path}/attachments/${encodeURIComponent(id(referenced?.id))}`)
context.addStep(MODULE, '删除重新引用的拖车附件', deleteReferencedResponse,
deleteReferencedResponse.http === 200 && codeOf(deleteReferencedResponse) === '200',
responseEvidence(deleteReferencedResponse), OPTIONS)
const replacementForm = new FormData(); replacementForm.append('attachmentType', 'DISPATCH_ORDER')
replacementForm.append('file', new Blob([minimalPdfBytes(`${context.marker}-DISPATCH_ORDER-REPLACEMENT`)],
{ type: 'application/pdf' }), `dispatch-order-replacement-${identity.suffix}.pdf`)
const replacementResponse = await context.requestMultipart('POST', `${path}/attachments/upload`,
replacementForm, context.token)
const replacement = context.apiSuccess(MODULE, '恢复一份DISPATCH_ORDER拖车附件供后续状态流转',
replacementResponse, '', OPTIONS)
context.assert(MODULE, '恢复附件使用新引用且仍指向平台文件', replacementResponse,
Boolean(id(replacement?.id) && id(replacement?.fileId)) && id(replacement?.id) !== id(attachment?.id),
`attachmentId=${id(replacement?.id) || 'missing'}, fileId=${id(replacement?.fileId) || 'missing'}`, OPTIONS)
const editedResponse = await request(context, 'PUT', path,
truckAppointmentPayload(context, identity.suffix, identity, Number(appointment.version), { currency: 'USD', payableAmount: 12.34 }))
@@ -254,9 +293,10 @@ export async function run(context) {
const finalDetailResponse = await request(context, 'GET', path)
const finalDetail = context.apiSuccess(MODULE, '回查COMPLETED订舱级拖车详情', finalDetailResponse, '', OPTIONS)
context.assert(MODULE, '完整生命周期完成且详情无柜次协议', finalDetailResponse,
finalDetail?.status === 'COMPLETED' && !hasForbiddenTruckFields(finalDetail)
finalDetail?.status === 'COMPLETED' && truckAppointmentFieldGaps(finalDetail).length === 0
&& !hasForbiddenTruckFields(finalDetail)
&& id(finalDetail?.oceanBookingId) === identity.oceanBookingId,
`appointmentNo=${finalDetail?.appointmentNo || 'missing'}, status=${finalDetail?.status || 'missing'}`, OPTIONS)
`appointmentNo=${finalDetail?.appointmentNo || 'missing'}, status=${finalDetail?.status || 'missing'}, fieldGaps=${truckAppointmentFieldGaps(finalDetail).join('|') || 'none'}`, OPTIONS)
const afterCompleteCandidate = await candidate(context, identity)
context.assert(MODULE, 'COMPLETED后订舱恢复候选', afterCompleteCandidate.response, Boolean(afterCompleteCandidate.item),
`oceanBookingNo=${identity.oceanBookingNo}`, OPTIONS)