优化
This commit is contained in:
@@ -37,7 +37,7 @@ export async function run(context) {
|
||||
|
||||
## 可用上下文
|
||||
|
||||
`dropship-pallet-matrix.mjs` 验证下述四场景整托手工加入、整托移除、智能推荐、独立订舱与文件放舱、真实柜号关联、预装确认、PDA 整托拣货和五类证据上传;异步装柜完成必须等到任务 `COMPLETED` 并逐票回查 `LOADED`。智能装柜的托盘汇总与装柜详情逐票组成分别核对,不能把共托汇总行的首票编号当作全托归属。随后经正式报关、国内离仓及人工船节点接口推进至 `PICKUP_READY`,再按柜生成逐票海外入库计划,检查来源托盘完整绑定、共享托盘身份一致和同键重放。离线合同为 `dropship-pallet-matrix-contract.test.mjs`;尚未包含海外实收和专用出库验收。
|
||||
`dropship-pallet-matrix.mjs` 验证下述四场景整托手工加入、整托移除、智能推荐、独立订舱与文件放舱、真实柜号关联、预装确认、PDA 整托拣货和五类证据上传;异步装柜完成必须等到任务 `COMPLETED` 并逐票回查 `LOADED`。智能装柜的托盘汇总与装柜详情逐票组成分别核对,不能把共托汇总行的首票编号当作全托归属。随后经正式报关、国内离仓及人工船节点接口推进至 `PICKUP_READY`,再按柜生成逐票海外入库计划,检查来源托盘完整绑定、共享托盘身份一致和同键重放。海外收货使用本轮创建并绑定接柜仓的 PDA 操作员,上传三类照片、签收、逐实物托盘收货、逐票箱数核平和完成入库,同键重放及同托新键扫码均不得重复入账;finally 停用本轮操作员。离线合同为 `dropship-pallet-matrix-contract.test.mjs`;尚未包含专用出库验收。
|
||||
|
||||
- `dropship-pallet-matrix` 通过总入口 `--modules dropship-pallet-matrix` 执行,自动依赖客户资质准备。通过正式 HTTP 创建五票订单,覆盖单票多托、单票单托、单票移箱合托、多票共享开放托盘;会创建并保留测试数据,不自动清理。报告 `createdResources.dropshipPalletMatrix` 保存逐票箱唛、托盘及各阶段证据,失败即停止并保留阻断;重跑创建新批次,不借用历史资源。`fullFlowVerified=false`,不能代替海外收货及一件代发专用出库的完整验收。
|
||||
|
||||
@@ -120,8 +120,8 @@ export async function run(context) {
|
||||
| PDA 人工领取、逐箱扫码、原子批量提交与待复核核验 | `manual-pda-batch-fixture.mjs`、`manual-pda-batch-verify.mjs`(可选,不进入默认全量) | `manual-pda-batch-contract.test.mjs` |
|
||||
| 五类装柜证据、PDA 最终完成与 LOADED 终态核验 | `manual-loading-complete-fixture.mjs`、`manual-loading-complete-verify.mjs`(可选,不进入默认全量) | `manual-loading-complete-contract.test.mjs` |
|
||||
| 订舱共享主数据前置与订舱验收 | `ocean-booking-fixture.mjs`、`ocean-bookings.mjs`、`phase3-port-schedules.mjs`、`phase3-carrier-schedule-queries.mjs`(DRAFT 可编辑;RELEASED 冻结;船司查询只创建并写当前 marker 登记任务) | `ocean-booking-prototype-contract.test.mjs`、`ocean-booking-fixture-contract.test.mjs`、`phase3-carrier-schedule-queries.test.mjs` |
|
||||
| 订舱级拖车预约完整生命周期 | `truck-appointments.mjs`(自包含创建并放舱,不创建或关联柜次;每订舱最多一张非终态拖车单;覆盖候选、附件、费用、幂等及 `SUBMIT → DISPATCH → PICKUP → ARRIVE → LOAD → DEPART → COMPLETE`) | `truck-appointments-contract.test.mjs` |
|
||||
| 订舱与拖车服务中心跨链路 | `service-center-flow.mjs`(依赖 `ocean-bookings,truck-appointments`;复用本轮 `RELEASED` 订舱创建订舱级拖车,验证派车单、完整状态链、历史、审计和关联零漂移) | 由入口依赖合同和拖车直接合同共同覆盖;真实 HTTP 缺本轮订舱或承运商时 fail-closed |
|
||||
| 订舱级拖车预约完整生命周期 | `truck-appointments.mjs`(自包含创建并放舱,不创建或关联柜次;已有有效或已完成拖车单的订舱不可重复创建;覆盖候选、附件、人民币费用、幂等及 `SUBMIT → DISPATCH → PICKUP → ARRIVE → LOAD`,拒绝手工离仓/完成;自动完成与应付由真实国内离仓链路验证) | `truck-appointments-contract.test.mjs` |
|
||||
| 订舱与拖车服务中心跨链路 | `service-center-flow.mjs`(依赖 `ocean-bookings,truck-appointments`;复用本轮 `RELEASED` 订舱创建订舱级拖车,验证派车单、离仓前状态链、历史、审计和关联零漂移) | 由入口依赖合同和拖车直接合同共同覆盖;真实 HTTP 缺本轮订舱或承运商时 fail-closed |
|
||||
| Admin 补充能力与保护边界 | `supplemental.mjs`、`supplemental-boundaries.mjs` | `supplemental-boundaries-contract.test.mjs`;只按本轮资源验证补充能力、负向和保护边界 |
|
||||
| 装柜清单三备注 | `orders.mjs`、`phase3-loading.mjs`(客户备注、最新内部备注、仓库备注;仓库备注 PATCH/清空/精确重放/旧版本零写) | `loading-list-remarks-contract.test.mjs` |
|
||||
| 消息中心管理 | `notifications.mjs` | `notifications-contract.test.mjs` |
|
||||
@@ -280,3 +280,6 @@ node scripts/admin-test-env-acceptance.mjs --modules merchant-qualification-fixt
|
||||
```powershell
|
||||
node --test scripts/admin-test-env-modules/merchant-qualification-fixture-contract.test.mjs
|
||||
```
|
||||
|
||||
|
||||
国内拖车自动完成与应付回归由 `phase3-outbound-dispatch.mjs` 在真实离仓后只读核验:订舱全部柜数、逐柜离仓、唯一 `AUTO_COMPLETE`、按拖车编号追溯柜次应付、金额守恒及 Finance 来源入池(有界等待 Outbox)。该模块的原有确认/撤销边界操作仍会写目标环境,执行须获得对应环境授权。`manual-state-ocean-trucking.mjs` 只构造可人工操作的状态,不伪造已完成拖车;自动终态由上述离仓链路覆盖。
|
||||
|
||||
@@ -62,3 +62,14 @@ test('overseas plans preserve per-inbound ownership and shared physical identity
|
||||
assert.match(source, /byPallet.get\(pallet.palletNo\) === identity/)
|
||||
assert.match(source, /JSON.stringify\(replay\) === JSON.stringify\(plans\)/)
|
||||
})
|
||||
|
||||
test('overseas receipt uses scoped PDA identity and scans each physical pallet once', () => {
|
||||
assert.match(source, /await createPicker\(context/)
|
||||
assert.match(source, /await disablePicker\(context/)
|
||||
assert.match(source, /const sourceNos = \[\.\.\.new Set\(group.flatMap/)
|
||||
assert.match(source, /current\/receipts\/pallet/)
|
||||
assert.match(source, /duplicate.result === 'ALREADY_RECEIVED'/)
|
||||
assert.match(source, /Number\(inbound.receivedPackages\) === item.expectedCartonMarks.length/)
|
||||
assert.match(source, /completed.status === 'COMPLETED'/)
|
||||
assert.match(source, /fullFlowVerified: false/)
|
||||
})
|
||||
|
||||
@@ -148,6 +148,7 @@ export async function run(context) {
|
||||
check(`${label}真实船司和订舱代理齐全`, null, Boolean(carrier?.id && agent?.id))
|
||||
const suffix = markerSuffix(context.marker)
|
||||
const draft = oceanBookingDraft(context, suffix, {
|
||||
customerId: String(group[0].task.tenantId), customerName: summary.customerName,
|
||||
warehouseId: String(summary.warehouseId), channelCode: summary.channelCode,
|
||||
}, { carrierCode: 'MATSON', shippingCompanyId: String(carrier.id), shippingCompanyName: carrier.name,
|
||||
bookingAgentId: String(agent.id), bookingAgentName: agent.name })
|
||||
|
||||
@@ -5,7 +5,7 @@ const TRUCK_BASE = '/api/platform/v1/truck-appointments'
|
||||
const OCEAN_LIST_STATUSES = ['DRAFT', 'RELEASED']
|
||||
const OCEAN_STATUSES = [...OCEAN_LIST_STATUSES]
|
||||
const TRUCK_STATUSES = ['DRAFT', 'PENDING_DISPATCH', 'DISPATCHED', 'PICKUP_IN_PROGRESS', 'ARRIVED_WAREHOUSE',
|
||||
'LOADING', 'DEPARTED', 'COMPLETED', 'RESCHEDULED', 'EXCEPTION', 'CANCELLED']
|
||||
'LOADING', 'RESCHEDULED', 'EXCEPTION', 'CANCELLED']
|
||||
|
||||
function id(value) { return value == null ? '' : String(value) }
|
||||
function code(response) { return String(response?.body?.code ?? 'missing') }
|
||||
@@ -153,7 +153,7 @@ async function buildTruck(context, oceanBooking, status, index) {
|
||||
if (status === 'EXCEPTION') return truckAction(context, value, 'exception')
|
||||
if (status === 'CANCELLED') return truckAction(context, value, 'cancel')
|
||||
const chain = [['submit', 'PENDING_DISPATCH'], ['dispatch', 'DISPATCHED'], ['pickup', 'PICKUP_IN_PROGRESS'],
|
||||
['arrive', 'ARRIVED_WAREHOUSE'], ['load', 'LOADING'], ['depart', 'DEPARTED'], ['complete', 'COMPLETED']]
|
||||
['arrive', 'ARRIVED_WAREHOUSE'], ['load', 'LOADING']]
|
||||
for (const [endpoint, target] of chain) {
|
||||
value = await truckAction(context, value, endpoint)
|
||||
if (target === status) return value
|
||||
|
||||
@@ -29,6 +29,70 @@ async function request(context, method, path, body, headers = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyTruckDeparturePayables(context, aggregate) {
|
||||
const appointmentId = id(aggregate?.truckAppointmentId)
|
||||
if (!appointmentId) return
|
||||
const truckResponse = await request(context, 'GET', `/api/platform/v1/truck-appointments/${appointmentId}`)
|
||||
const truck = context.apiSuccess(MODULE, '回查国内离仓关联拖车', truckResponse, '', OPTIONS)
|
||||
if (!truck) return
|
||||
const bookingResponse = await request(context, 'GET', `/api/platform/v1/ocean-bookings/${id(truck.oceanBookingId)}`)
|
||||
const booking = context.apiSuccess(MODULE, '核对拖车订舱全部要求货柜', bookingResponse, '', OPTIONS)
|
||||
if (!booking) return
|
||||
const expectedCount = (booking.containerRequirements || []).reduce((sum, item) => sum + Number(item.quantity), 0)
|
||||
const departed = []
|
||||
for (const container of booking.containers || []) {
|
||||
const response = await request(context, 'GET', `${BASE}/${id(container.containerBookingId)}`)
|
||||
const detail = context.apiSuccess(MODULE, '回查拖车关联货柜离仓事实', response, '', OPTIONS)
|
||||
if (detail?.aggregate) departed.push(detail.aggregate)
|
||||
}
|
||||
const ready = expectedCount > 0 && departed.length === expectedCount
|
||||
&& departed.every(item => item.dispatchStatus === 'WAITING_FOR_LOADING')
|
||||
context.assert(MODULE, '全部要求货柜离仓才自动完成拖车', truckResponse,
|
||||
ready ? truck.status === 'COMPLETED' : truck.status !== 'COMPLETED',
|
||||
`ready=${ready}, status=${truck.status}`, OPTIONS)
|
||||
if (!ready || truck.status !== 'COMPLETED') return
|
||||
const historyResponse = await request(context, 'GET', `/api/platform/v1/truck-appointments/${appointmentId}/history`)
|
||||
const history = context.apiSuccess(MODULE, '回查拖车自动完成历史', historyResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '拖车自动完成仅记录一次', historyResponse,
|
||||
Array.isArray(history) && history.filter(item => item.action === 'AUTO_COMPLETE').length === 1, '', OPTIONS)
|
||||
const fees = new Map()
|
||||
for (const container of departed) {
|
||||
const customers = [...new Set(container.inbounds.map(item => id(item.customerId)))]
|
||||
for (const customer of customers) {
|
||||
const response = await request(context, 'GET', `/api/platform/v1/loading-plans/${id(container.loadingPlanId)}/fee-details?accountId=${customer}`)
|
||||
const details = context.apiSuccess(MODULE, '回查拖车生成的柜次应付', response, '', OPTIONS)
|
||||
for (const fee of Array.isArray(details) ? details : []) {
|
||||
if (fee.direction === 'PAYABLE' && String(fee.remark || '').includes(`国内拖车单 ${truck.appointmentNo};`)) fees.set(id(fee.id), fee)
|
||||
}
|
||||
}
|
||||
}
|
||||
const totalCents = [...fees.values()].reduce((sum, fee) => sum + Math.round(Number(fee.amount) * 100), 0)
|
||||
context.assert(MODULE, '拖车应付分摊守恒且自动审核', null,
|
||||
totalCents === Math.round(Number(truck.payableAmount || 0) * 100)
|
||||
&& [...fees.values()].every(fee => fee.currency === 'CNY' && fee.reviewStatus === 'APPROVED'
|
||||
&& fee.settlementMode === 'ON_ACCOUNT' && id(fee.supplierId) === id(truck.carrierId)),
|
||||
`sourceCount=${fees.size}, totalCents=${totalCents}`, OPTIONS)
|
||||
const imported = new Map()
|
||||
for (let attempt = 0; attempt < 12 && imported.size < fees.size; attempt++) {
|
||||
for (const customer of new Set([...fees.values()].map(fee => id(fee.tenantId)))) {
|
||||
for (let page = 1; ; page++) {
|
||||
const response = await request(context, 'GET', `/api/platform/finance/v1/fee-sources?customerId=${customer}&direction=PAYABLE&page=${page}&pageSize=200`)
|
||||
const rows = context.apiSuccess(MODULE, '回查拖车应付财务入池', response, '', OPTIONS)
|
||||
if (!Array.isArray(rows)) break
|
||||
rows.filter(row => row.sourceSystem === 'FIRSTLEG' && row.sourceType === 'CONTAINER_FEE'
|
||||
&& fees.has(id(row.sourceFeeId))).forEach(row => imported.set(id(row.sourceFeeId), row))
|
||||
if (page * 200 >= Number(response.body?.page?.total || 0)) break
|
||||
}
|
||||
}
|
||||
if (imported.size < fees.size) await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
}
|
||||
context.assert(MODULE, '拖车费用真实流转到财务应付来源', null,
|
||||
imported.size === fees.size && [...imported.values()].every(row => row.currency === 'CNY'
|
||||
&& row.direction === 'PAYABLE' && row.reviewStatus === 'APPROVED'
|
||||
&& Math.round(Number(row.amount) * 100) === Math.round(Number(fees.get(id(row.sourceFeeId)).amount) * 100)),
|
||||
`expected=${fees.size}, imported=${imported.size}`, OPTIONS)
|
||||
}
|
||||
|
||||
export async function run(context) {
|
||||
const fixture = context.createdResources?.phase3Loading
|
||||
|| context.createdResources?.phase3ContainerLoading
|
||||
@@ -80,6 +144,7 @@ export async function run(context) {
|
||||
`truck=${aggregate?.truckAppointmentStatus || 'none'}, ocean=${aggregate?.oceanBookingStatus || 'none'}`, OPTIONS)
|
||||
|
||||
if (aggregate?.dispatchStatus === 'WAITING_FOR_LOADING') {
|
||||
await verifyTruckDeparturePayables(context, aggregate)
|
||||
context.assert(MODULE, '整柜报关完成后系统已自动确认国内离仓', detailResponse,
|
||||
aggregate?.inbounds?.every?.((item) => item?.inboundStatus === 'LOADED')
|
||||
&& detail?.timeline?.some?.((item) => item.module === 'OUTBOUND_DISPATCH'
|
||||
@@ -178,6 +243,7 @@ export async function run(context) {
|
||||
&& item.action === 'REVERSE_DISPATCH'),
|
||||
`dispatch=${finalDetail?.aggregate?.dispatchStatus || 'missing'}, inbounds=${finalDetail?.aggregate?.inbounds?.length ?? 'missing'}, version=${finalDetail?.aggregate?.version ?? 'missing'}`, OPTIONS)
|
||||
if (confirmed?.aggregate) {
|
||||
await verifyTruckDeparturePayables(context, confirmed.aggregate)
|
||||
context.createdResources.phase3OutboundDispatch = {
|
||||
marker: context.marker, inboundId, containerBookingId,
|
||||
orderId: id(inboundBinding?.orderId), tenantId: id(inboundBinding?.tenantId),
|
||||
|
||||
@@ -114,8 +114,6 @@ export async function run(context) {
|
||||
['跨链路开始提货', 'pickup', 'PICKUP_IN_PROGRESS'],
|
||||
['跨链路拖车到仓', 'arrive', 'ARRIVED_WAREHOUSE'],
|
||||
['跨链路开始装货', 'load', 'LOADING'],
|
||||
['跨链路拖车离仓', 'depart', 'DEPARTED'],
|
||||
['完成跨链路拖车', 'complete', 'COMPLETED'],
|
||||
]) {
|
||||
const action = await actionRequest(context, path, endpoint, version)
|
||||
const changed = context.apiSuccess(MODULE, name, action, '', OPTIONS)
|
||||
@@ -125,9 +123,9 @@ export async function run(context) {
|
||||
version = Number(changed.version)
|
||||
}
|
||||
const detail = await context.request('GET', `${TRUCK}/${appointmentId}`)
|
||||
const final = context.apiSuccess(MODULE, '回查跨服务链路安全终态', detail, '', OPTIONS)
|
||||
context.assert(MODULE, '跨服务链路终态持久化且关联订舱未漂移', detail,
|
||||
final?.status === 'COMPLETED' && id(final?.oceanBookingId) === id(ocean.id)
|
||||
const final = context.apiSuccess(MODULE, '回查跨服务链路离仓前状态', detail, '', OPTIONS)
|
||||
context.assert(MODULE, '跨服务链路离仓前状态持久化且关联订舱未漂移', detail,
|
||||
final?.status === 'LOADING' && id(final?.oceanBookingId) === id(ocean.id)
|
||||
&& !Object.hasOwn(final || {}, 'containerBookingId')
|
||||
&& !Object.hasOwn(final || {}, 'loadingPlanId'),
|
||||
`status=${final?.status || 'missing'}, oceanBookingId=${final?.oceanBookingId || 'missing'}, containerBookingId=${final?.containerBookingId || 'n/a'}, loadingPlanId=${final?.loadingPlanId || 'n/a'}`, OPTIONS)
|
||||
@@ -135,19 +133,19 @@ export async function run(context) {
|
||||
const historyResponse = await context.request('GET', `${TRUCK}/${appointmentId}/history`)
|
||||
const history = context.apiSuccess(MODULE, '查询跨服务链路拖车预约状态历史', historyResponse, '', OPTIONS)
|
||||
const actions = Array.isArray(history) ? history.filter((item) => item?.action !== 'CREATE') : []
|
||||
const expectedActions = ['SUBMIT', 'DISPATCH', 'PICKUP', 'ARRIVE', 'LOAD', 'DEPART', 'COMPLETE']
|
||||
const expectedActions = ['SUBMIT', 'DISPATCH', 'PICKUP', 'ARRIVE', 'LOAD']
|
||||
context.assert(MODULE, '跨服务链路历史包含完整状态链、操作者、时间和备注', historyResponse,
|
||||
expectedActions.every((action) => actions.some((item) => item.action === action
|
||||
&& id(item.appointmentId) === appointmentId && Boolean(item.operatorId)
|
||||
&& Boolean(item.operatorName) && Boolean(item.createdAt) && item.reason === context.marker)),
|
||||
`history=${history?.length ?? 'n/a'}, actions=${actions.map((item) => item.action).join(',')}`,
|
||||
OPTIONS)
|
||||
if (final?.status === 'COMPLETED') {
|
||||
if (final?.status === 'LOADING') {
|
||||
context.createdResources.serviceCenterFlow = {
|
||||
appointmentId, oceanBookingId: ocean.id,
|
||||
containerBookingId: null,
|
||||
loadingPlanId: null,
|
||||
marker: context.marker, finalStatus: 'COMPLETED',
|
||||
marker: context.marker, finalStatus: 'LOADING',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,15 +53,15 @@ test('HTTP lifecycle covers uniqueness idempotency attachments and every action'
|
||||
'重复创建未改变版本和状态', '上传并关联DISPATCH_ORDER拖车附件', '查询拖车附件列表',
|
||||
'删除首次上传的拖车附件引用', '重新引用平台归属拖车附件',
|
||||
'删除重新引用的拖车附件', '恢复一份DISPATCH_ORDER拖车附件供后续状态流转',
|
||||
'编辑订舱级拖车为USD应付费用', '整单推进SUBMIT', 'SUBMIT同键同载荷重放',
|
||||
'编辑订舱级拖车为RMB应付费用', '整单推进SUBMIT', 'SUBMIT同键同载荷重放',
|
||||
'SUBMIT重放版本零漂移', 'SUBMIT同键异载荷A0409',
|
||||
'COMPLETED后订舱恢复候选', 'COMPLETED后同订舱可新建下一张拖车单',
|
||||
'禁止手工离仓或完成拖车',
|
||||
'CANCELLED后订舱恢复候选', 'CANCELLED后同订舱可再次新建',
|
||||
'EXCEPTION后订舱恢复候选', 'EXCEPTION后同订舱可再次新建']) {
|
||||
assert.match(source, new RegExp(label), label)
|
||||
}
|
||||
assert.match(source, /\['DISPATCH', '派车'\], \['PICKUP', '提柜'\], \['ARRIVE', '到仓'\]/)
|
||||
assert.match(source, /\['LOAD', '装货'\], \['DEPART', '离仓'\], \['COMPLETE', '完成'\]/)
|
||||
assert.match(source, /\['LOAD', '装货'\]/)
|
||||
assert.match(source, /context\.apiSuccess\(MODULE, `整单推进\$\{name\}`/)
|
||||
})
|
||||
|
||||
|
||||
@@ -270,10 +270,10 @@ export async function run(context) {
|
||||
`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 }))
|
||||
appointment = context.apiSuccess(MODULE, '编辑订舱级拖车为USD应付费用', editedResponse, '', OPTIONS)
|
||||
truckAppointmentPayload(context, identity.suffix, identity, Number(appointment.version), { currency: 'RMB', payableAmount: 12.34 }))
|
||||
appointment = context.apiSuccess(MODULE, '编辑订舱级拖车为RMB应付费用', editedResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '编辑响应费用正确且无柜次协议', editedResponse,
|
||||
appointment?.currency === 'USD' && Number(appointment?.payableAmount) === 12.34 && !hasForbiddenTruckFields(appointment), '', OPTIONS)
|
||||
appointment?.currency === 'RMB' && Number(appointment?.payableAmount) === 12.34 && !hasForbiddenTruckFields(appointment), '', OPTIONS)
|
||||
|
||||
const submitKey = `${context.marker}-truck-submit`.slice(0, 128)
|
||||
const submitBody = { expectedVersion: Number(appointment.version), reason: '幂等提交' }
|
||||
@@ -289,27 +289,24 @@ export async function run(context) {
|
||||
codeOf(conflictResponse) === 'A0409', `code=${codeOf(conflictResponse)}`, OPTIONS)
|
||||
|
||||
for (const [name, reason] of [['DISPATCH', '派车'], ['PICKUP', '提柜'], ['ARRIVE', '到仓'],
|
||||
['LOAD', '装货'], ['DEPART', '离仓'], ['COMPLETE', '完成']]) {
|
||||
['LOAD', '装货']]) {
|
||||
appointment = await action(context, path, appointment, name, reason)
|
||||
context.assert(MODULE, `${name}响应无柜次协议`, null, Boolean(appointment) && !hasForbiddenTruckFields(appointment),
|
||||
`status=${appointment?.status || 'missing'}`, OPTIONS)
|
||||
}
|
||||
const finalDetailResponse = await request(context, 'GET', path)
|
||||
const finalDetail = context.apiSuccess(MODULE, '回查COMPLETED订舱级拖车详情', finalDetailResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '完整生命周期完成且详情无柜次协议', finalDetailResponse,
|
||||
finalDetail?.status === 'COMPLETED' && truckAppointmentFieldGaps(finalDetail).length === 0
|
||||
const finalDetail = context.apiSuccess(MODULE, '回查待离仓订舱级拖车详情', finalDetailResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '未确认国内离仓时保持装柜中且详情无柜次协议', finalDetailResponse,
|
||||
finalDetail?.status === 'LOADING' && truckAppointmentFieldGaps(finalDetail).length === 0
|
||||
&& !hasForbiddenTruckFields(finalDetail)
|
||||
&& id(finalDetail?.oceanBookingId) === identity.oceanBookingId,
|
||||
`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)
|
||||
const rebuiltResponse = await request(context, 'POST', TRUCKS, base)
|
||||
let rebuilt = context.apiSuccess(MODULE, 'COMPLETED后同订舱可新建下一张拖车单', rebuiltResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '重建拖车单保持订舱级响应', rebuiltResponse,
|
||||
rebuilt?.status === 'DRAFT' && !hasForbiddenTruckFields(rebuilt)
|
||||
&& id(rebuilt?.oceanBookingId) === identity.oceanBookingId,
|
||||
`appointmentNo=${rebuilt?.appointmentNo || 'missing'}`, OPTIONS)
|
||||
for (const obsoleteAction of ['depart', 'complete']) {
|
||||
const rejected = await request(context, 'POST', `${path}/${obsoleteAction}`,
|
||||
{ expectedVersion: Number(finalDetail.version) }, { 'Idempotency-Key': `${context.marker}-${obsoleteAction}`.slice(0, 128) })
|
||||
context.assert(MODULE, '禁止手工离仓或完成拖车', rejected, codeOf(rejected) === 'A0409', '', OPTIONS)
|
||||
}
|
||||
let rebuilt = finalDetail
|
||||
const cancelled = await action(context, `${TRUCKS}/${id(rebuilt?.id)}`, rebuilt, 'CANCEL', '取消测试单')
|
||||
const afterCancelCandidate = await candidate(context, identity)
|
||||
context.assert(MODULE, 'CANCELLED后订舱恢复候选', afterCancelCandidate.response,
|
||||
|
||||
@@ -466,8 +466,8 @@ Assert-NotContains $truckOpenApi 'customerId:' '拖车 OpenAPI 客户字段'
|
||||
Assert-NotContains $truckOpenApi 'containerBookingId:' '拖车 OpenAPI 柜次字段'
|
||||
Assert-NotContains $truckOpenApi 'loadingPlanId:' '拖车 OpenAPI 装柜计划字段'
|
||||
Assert-Contains $truckOpenApi 'Each booking may have at most one non-terminal appointment' '拖车订舱级唯一性'
|
||||
Assert-Contains $truckOpenApi 'PDA loading completion does not change a booking-level truck appointment' '拖车与 PDA 生命周期解耦'
|
||||
Assert-Contains $truckOpenApi 'excluding bookings that already have a non-terminal truck appointment' '拖车创建候选过滤'
|
||||
Assert-Contains $truckOpenApi 'PDA loading completion alone does not complete the appointment' '拖车与 PDA 生命周期解耦'
|
||||
Assert-Contains $truckOpenApi 'excluding bookings that already have an active or completed truck appointment' '拖车创建候选过滤'
|
||||
Assert-Contains $firstlegAgents '目的港码头拖车与还空预警' 'Firstleg 目的港码头拖车入口'
|
||||
Assert-Contains $firstlegAgents '按卸柜完成时间加 72 小时' 'Firstleg 还空预警口径'
|
||||
Assert-Contains $terminalDrayageOpenApi '/api/platform/v1/terminal-drayages/{containerBookingId}/fleet:' '码头拖车车队命令 OpenAPI'
|
||||
|
||||
Reference in New Issue
Block a user