优化
This commit is contained in:
@@ -121,6 +121,16 @@ node --test scripts/admin-test-env-modules/suppliers-contract.test.mjs
|
||||
node --test scripts/admin-test-env-modules/domestic-container-contract.test.mjs
|
||||
```
|
||||
|
||||
## 货柜履约列表字段合同
|
||||
|
||||
`fulfillment-list-field-contract.mjs` 统一校验货柜、预装/装柜清单、订舱、船期、拖车、报关协同和国内出仓正式响应中的稳定 ID、非空展示字段、非负数量/重量/体积及 ISO 时间格式。各生产模块在已取得的本轮 marker 响应上执行断言,不额外借用历史数据;未冻结的原型字段只登记在 `docs/三期/货柜履约原型字段验收矩阵-2026-08-17.md` 的 `UNKNOWN`,不得由 fixture 伪造。
|
||||
|
||||
离线合同:
|
||||
|
||||
```powershell
|
||||
node --test scripts/admin-test-env-modules/fulfillment-prototype-field-matrix-contract.test.mjs
|
||||
```
|
||||
|
||||
## 清库后的商户下单前置
|
||||
|
||||
`merchant-qualification-fixture.mjs` 是默认全量验收的首个模块。它通过平台正式接口创建本轮隔离客户,经 File 上传真实营业执照和法人身份证文件,再走 IAM 提交审核和平台审核通过;审核完成后把本轮动态加载模块使用的商户账号切换到该客户。
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
function valueAt(record, path) {
|
||||
return path.split('.').reduce((value, key) => value?.[key], record)
|
||||
}
|
||||
|
||||
export function isPositiveId(value) {
|
||||
return /^\d+$/.test(String(value ?? '')) && BigInt(String(value)) > 0n
|
||||
}
|
||||
|
||||
export function isNonBlank(value) {
|
||||
return typeof value === 'string' && value.trim().length > 0
|
||||
}
|
||||
|
||||
export function isIsoDateTime(value) {
|
||||
return isNonBlank(value) && Number.isFinite(Date.parse(value))
|
||||
}
|
||||
|
||||
export function isNonNegativeNumber(value) {
|
||||
return value !== null && value !== '' && Number.isFinite(Number(value)) && Number(value) >= 0
|
||||
}
|
||||
|
||||
function gaps(record, rules) {
|
||||
return Object.entries(rules)
|
||||
.filter(([path, predicate]) => !predicate(valueAt(record, path), record))
|
||||
.map(([path]) => path)
|
||||
}
|
||||
|
||||
export function containerFieldGaps(record, { requireContainerNo = false } = {}) {
|
||||
return gaps(record, {
|
||||
id: isPositiveId,
|
||||
cabinetNo: isNonBlank,
|
||||
warehouseId: isPositiveId,
|
||||
warehouseName: isNonBlank,
|
||||
channelCode: isNonBlank,
|
||||
containerType: isNonBlank,
|
||||
capacityCbm: (value) => Number(value) > 0,
|
||||
maxWeightKg: (value) => Number(value) > 0,
|
||||
planStatus: isNonBlank,
|
||||
...(requireContainerNo ? { containerNo: isNonBlank } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
export function preloadFieldGaps(record) {
|
||||
return gaps(record, {
|
||||
planId: isPositiveId,
|
||||
containerBookingId: isPositiveId,
|
||||
preloadNo: isNonBlank,
|
||||
cabinetNo: isNonBlank,
|
||||
customers: (value) => Array.isArray(value) && value.length > 0
|
||||
&& value.every((customer) => isPositiveId(customer?.accountId) && isNonBlank(customer?.customerName)),
|
||||
warehouseName: isNonBlank,
|
||||
containerType: isNonBlank,
|
||||
status: isNonBlank,
|
||||
skuCount: isNonNegativeNumber,
|
||||
totalPackageCount: isNonNegativeNumber,
|
||||
totalVolumeCbm: isNonNegativeNumber,
|
||||
totalGrossWeightKg: isNonNegativeNumber,
|
||||
createdAt: isIsoDateTime,
|
||||
})
|
||||
}
|
||||
|
||||
export function loadingListFieldGaps(record) {
|
||||
return gaps(record, {
|
||||
planId: isPositiveId,
|
||||
containerBookingId: isPositiveId,
|
||||
loadingListNo: isNonBlank,
|
||||
containerNo: isNonBlank,
|
||||
cabinetNo: isNonBlank,
|
||||
warehouseName: isNonBlank,
|
||||
status: (value) => value === 'COMPLETED',
|
||||
totalPackageCount: isNonNegativeNumber,
|
||||
totalGrossWeightKg: isNonNegativeNumber,
|
||||
totalNetWeightKg: isNonNegativeNumber,
|
||||
totalVolumeCbm: isNonNegativeNumber,
|
||||
totalQuantity: isNonNegativeNumber,
|
||||
createdAt: isIsoDateTime,
|
||||
})
|
||||
}
|
||||
|
||||
export function oceanBookingFieldGaps(record) {
|
||||
return gaps(record, {
|
||||
id: isPositiveId,
|
||||
bookingNo: isNonBlank,
|
||||
status: (value) => ['CONFIRMED', 'CHANGE_PENDING', 'RELEASED', 'CANCELLED'].includes(value),
|
||||
vesselName: isNonBlank,
|
||||
voyageNo: isNonBlank,
|
||||
estimatedSailingAt: isIsoDateTime,
|
||||
estimatedArrivalAt: isIsoDateTime,
|
||||
customerName: (value, item) => item.customerId == null ? value == null : isNonBlank(value),
|
||||
shippingCompanyName: (value, item) => item.shippingCompanyId == null ? value == null : isNonBlank(value),
|
||||
bookingAgentName: (value, item) => item.bookingAgentId == null ? value == null : isNonBlank(value),
|
||||
})
|
||||
}
|
||||
|
||||
export function portScheduleFieldGaps(record) {
|
||||
const view = record?.portSchedule ?? record
|
||||
const schedule = view?.schedule ?? view
|
||||
return gaps(schedule, {
|
||||
id: isPositiveId,
|
||||
scheduleNo: isNonBlank,
|
||||
sourceName: isNonBlank,
|
||||
vesselName: isNonBlank,
|
||||
voyageNo: isNonBlank,
|
||||
departurePortCode: isNonBlank,
|
||||
destinationPortCode: isNonBlank,
|
||||
etdAt: isIsoDateTime,
|
||||
etaAt: isIsoDateTime,
|
||||
observedAt: isIsoDateTime,
|
||||
updatedAt: isIsoDateTime,
|
||||
expiresAt: isIsoDateTime,
|
||||
}).concat(typeof view?.expired === 'boolean' ? [] : ['expired'])
|
||||
}
|
||||
|
||||
export function truckAppointmentFieldGaps(record) {
|
||||
return gaps(record, {
|
||||
id: isPositiveId,
|
||||
appointmentNo: isNonBlank,
|
||||
oceanBookingId: isPositiveId,
|
||||
containerBookingId: isPositiveId,
|
||||
loadingPlanId: isPositiveId,
|
||||
warehouseId: isPositiveId,
|
||||
warehouseName: isNonBlank,
|
||||
plannedLoadingAt: isIsoDateTime,
|
||||
status: isNonBlank,
|
||||
customerName: (value, item) => item.customerId == null ? value == null : isNonBlank(value),
|
||||
carrierName: (value, item) => item.carrierId == null ? value == null : isNonBlank(value),
|
||||
})
|
||||
}
|
||||
|
||||
export function customsCaseFieldGaps(record) {
|
||||
const customsCase = record?.customsCase ?? record?.case ?? record
|
||||
return gaps(customsCase, {
|
||||
id: isPositiveId,
|
||||
customsNo: isNonBlank,
|
||||
orderId: isPositiveId,
|
||||
inboundId: isPositiveId,
|
||||
containerBookingId: isPositiveId,
|
||||
checklistVersionId: isPositiveId,
|
||||
checklistCode: isNonBlank,
|
||||
checklistVersionNo: isNonBlank,
|
||||
status: isNonBlank,
|
||||
updatedAt: isIsoDateTime,
|
||||
})
|
||||
}
|
||||
|
||||
export function outboundAggregateFieldGaps(record) {
|
||||
const aggregate = record?.aggregate ?? record
|
||||
return gaps(aggregate, {
|
||||
orderId: isPositiveId,
|
||||
inboundId: isPositiveId,
|
||||
inboundCode: isNonBlank,
|
||||
customerId: isPositiveId,
|
||||
customerName: isNonBlank,
|
||||
containerBookingId: isPositiveId,
|
||||
containerNo: isNonBlank,
|
||||
loadingPlanId: isPositiveId,
|
||||
loadingPlanStatus: isNonBlank,
|
||||
pickTaskId: isPositiveId,
|
||||
pickTaskStatus: isNonBlank,
|
||||
oceanBookingId: isPositiveId,
|
||||
oceanBookingStatus: isNonBlank,
|
||||
dispatchStatus: isNonBlank,
|
||||
version: isNonNegativeNumber,
|
||||
'gate.items': (value) => Array.isArray(value),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import test from 'node:test'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import {
|
||||
containerFieldGaps,
|
||||
customsCaseFieldGaps,
|
||||
loadingListFieldGaps,
|
||||
oceanBookingFieldGaps,
|
||||
outboundAggregateFieldGaps,
|
||||
portScheduleFieldGaps,
|
||||
preloadFieldGaps,
|
||||
truckAppointmentFieldGaps,
|
||||
} from './fulfillment-list-field-contract.mjs'
|
||||
|
||||
const repo = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..')
|
||||
const workspace = path.resolve(repo, '..')
|
||||
const read = (...parts) => fs.readFileSync(path.join(...parts), 'utf8')
|
||||
|
||||
const ids = {
|
||||
id: '2089000000000000001', plan: '2089000000000000002', container: '2089000000000000003',
|
||||
order: '2089000000000000004', inbound: '2089000000000000005', task: '2089000000000000006',
|
||||
}
|
||||
const at = '2026-08-17T08:00:00'
|
||||
|
||||
test('原型锁定货柜、预装、装柜、订舱与拖车关键列表字段', () => {
|
||||
const source = read(workspace, 'docs', '悦', '货代SAAS平台后台', 'prototype-src', 'modules', '06-containers.html')
|
||||
for (const label of ['集装箱号', '装柜清单号', '货柜状态', '柜号', '预装单号', '客户', '仓库',
|
||||
'船名航次', '箱型', '总毛重', '总体积', '订舱代理', '派车单号', '运输公司', '预计装箱日期']) {
|
||||
assert.match(source, new RegExp(label))
|
||||
}
|
||||
})
|
||||
|
||||
test('八类 Admin 列表均以左固定操作列开头', () => {
|
||||
const sources = [
|
||||
['container-loading/pages/ContainersView.vue', 1],
|
||||
['loading-lists/components/LoadingListsView.vue', 2],
|
||||
['ocean-bookings/pages/OceanBookingsView.vue', 1],
|
||||
['port-schedules/pages/PortSchedulesView.vue', 1],
|
||||
['trucking/pages/TruckAppointmentsView.vue', 1],
|
||||
['customs/pages/CustomsCasesView.vue', 1],
|
||||
['outbound-dispatch/pages/OutboundDispatchesView.vue', 1],
|
||||
]
|
||||
for (const [relative, count] of sources) {
|
||||
const source = read(workspace, 'apps', 'admin-web', 'src', 'modules', relative)
|
||||
assert.equal([...source.matchAll(/\{ key: 'actions', label: '操作',[^\n}]*fixed: 'left'|\{ key: 'actions', label: '操作', fixed: 'left'/g)].length, count,
|
||||
`${relative} operation column count`)
|
||||
}
|
||||
})
|
||||
|
||||
test('Admin 当前列表逐字段列与验收矩阵一致', () => {
|
||||
const expectations = {
|
||||
'container-loading/pages/ContainersView.vue': ['cabinetNo', 'oceanBooking', 'containerNo', 'planStatus', 'warehouseName', 'terminalTruckingCompany', 'billOfLadingNo', 'containerType', 'shippingCompanyName', 'estimatedSailingAt', 'actualPortArrivalAt'],
|
||||
'loading-lists/components/LoadingListsView.vue': ['preloadNo', 'customers', 'skuCount', 'loadingListNo', 'totalPackageCount', 'totalGrossWeightKg', 'totalNetWeightKg', 'totalVolumeCbm', 'totalQuantity', 'createdByName'],
|
||||
'ocean-bookings/pages/OceanBookingsView.vue': ['bookingNo', 'customerName', 'billOfLadingNo', 'carrierName', 'bookingAgent', 'vesselName', 'pol', 'pod', 'etd', 'eta', 'updatedAt'],
|
||||
'port-schedules/pages/PortSchedulesView.vue': ['schedule.scheduleNo', 'schedule.sourceName', 'schedule.vesselName', 'schedule.departurePortName', 'schedule.destinationPortName', 'schedule.etdAt', 'schedule.etaAt', 'schedule.observedAt', 'expired'],
|
||||
'trucking/pages/TruckAppointmentsView.vue': ['appointmentNo', 'containerNo', 'customerName', 'company', 'driver', 'vehicleNo', 'store', 'payableAmount', 'receivableAmount', 'plannedLoadingAt', 'attachments'],
|
||||
'customs/pages/CustomsCasesView.vue': ['customsNo', 'orderLink', 'inboundLink', 'checklistCode', 'checklistVersionNo', 'status', 'updatedAt'],
|
||||
'outbound-dispatch/pages/OutboundDispatchesView.vue': ['inboundCode', 'customerName', 'containerNo', 'oceanBookingStatus', 'truckAppointmentStatus', 'pickTaskStatus', 'dispatchStatus', 'gate'],
|
||||
}
|
||||
for (const [relative, fields] of Object.entries(expectations)) {
|
||||
const source = read(workspace, 'apps', 'admin-web', 'src', 'modules', relative)
|
||||
for (const field of fields) assert.match(source, new RegExp(field), `${relative}: ${field}`)
|
||||
}
|
||||
})
|
||||
|
||||
test('正式 DTO/OpenAPI 暴露矩阵使用的稳定字段', () => {
|
||||
const sources = [
|
||||
['firstleg/src/main/java/com/baiyue/firstleg/entity/ContainerBookingEntity.java', ['cabinetNo', 'containerNo', 'channelCode', 'warehouseName', 'capacityCbm', 'maxWeightKg', 'planStatus']],
|
||||
['firstleg/src/main/java/com/baiyue/firstleg/dto/LoadingListDtos.java', ['record PreloadSummary', 'record ConfirmedSummary', 'totalGrossWeightKg', 'totalVolumeCbm', 'customers']],
|
||||
['firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/dto/OceanBookingDtos.java', ['record Detail', 'record RealContainer', 'bookingAgentName', 'estimatedSailingAt', 'estimatedArrivalAt']],
|
||||
['firstleg/src/main/java/com/baiyue/firstleg/portschedule/dto/PortScheduleDtos.java', ['record ScheduleView', 'sourceName', 'etdAt', 'etaAt', 'expiresAt']],
|
||||
['firstleg/src/main/java/com/baiyue/firstleg/entity/TruckAppointmentEntity.java', ['appointmentNo', 'customerName', 'carrierName', 'plannedLoadingAt', 'containerNo']],
|
||||
['firstleg/src/main/java/com/baiyue/firstleg/customs/entity/CustomsCaseEntity.java', ['customsNo', 'checklistCode', 'checklistVersionNo', 'updatedAt']],
|
||||
['firstleg/src/main/java/com/baiyue/firstleg/outbound/dispatch/dto/OutboundDispatchDtos.java', ['record Aggregate', 'customerName', 'pickTaskStatus', 'dispatchStatus', 'GateResult']],
|
||||
]
|
||||
for (const [relative, fields] of sources) {
|
||||
const source = read(repo, relative)
|
||||
for (const field of fields) assert.match(source, new RegExp(field), `${relative}: ${field}`)
|
||||
}
|
||||
})
|
||||
|
||||
test('字段校验器接受正式完整投影并精确暴露缺字段', () => {
|
||||
const samples = [
|
||||
[containerFieldGaps, { id: ids.container, cabinetNo: 'CAB-1', containerNo: 'MSCU1234567', warehouseId: ids.id, warehouseName: '宁波仓', channelCode: 'EXX', containerType: '40HC', capacityCbm: 76, maxWeightKg: 28000, planStatus: 'EDITING' }, { requireContainerNo: true }, 'warehouseName'],
|
||||
[preloadFieldGaps, { planId: ids.plan, containerBookingId: ids.container, preloadNo: 'PL-1', cabinetNo: 'CAB-1', customers: [{ accountId: ids.id, customerName: '测试客户' }], warehouseName: '宁波仓', containerType: '40HC', status: 'EDITING', skuCount: 1, totalPackageCount: 1, totalVolumeCbm: 1, totalGrossWeightKg: 10, createdAt: at }, undefined, 'customers'],
|
||||
[loadingListFieldGaps, { planId: ids.plan, containerBookingId: ids.container, loadingListNo: 'LL-1', containerNo: 'MSCU1234567', cabinetNo: 'CAB-1', warehouseName: '宁波仓', status: 'COMPLETED', totalPackageCount: 1, totalGrossWeightKg: 10, totalNetWeightKg: 9, totalVolumeCbm: 1, totalQuantity: 2, createdAt: at }, undefined, 'containerNo'],
|
||||
[oceanBookingFieldGaps, { id: ids.id, bookingNo: 'BK-1', status: 'RELEASED', vesselName: 'VESSEL', voyageNo: 'V1', estimatedSailingAt: at, estimatedArrivalAt: at, customerId: ids.id, customerName: '测试客户', shippingCompanyId: null, shippingCompanyName: null, bookingAgentId: ids.id, bookingAgentName: '代理' }, undefined, 'voyageNo'],
|
||||
[portScheduleFieldGaps, { portSchedule: { schedule: { id: ids.id, scheduleNo: 'PS-1', sourceName: '船公司', vesselName: 'VESSEL', voyageNo: 'V1', departurePortCode: 'CNNGB', destinationPortCode: 'USLAX', etdAt: at, etaAt: at, observedAt: at, updatedAt: at, expiresAt: at }, expired: false } }, undefined, 'sourceName'],
|
||||
[truckAppointmentFieldGaps, { id: ids.id, appointmentNo: 'TA-1', oceanBookingId: ids.id, containerBookingId: ids.container, loadingPlanId: ids.plan, warehouseId: ids.id, warehouseName: '宁波仓', customerId: ids.id, customerName: '测试客户', carrierId: ids.id, carrierName: '拖车商', plannedLoadingAt: at, status: 'DRAFT' }, undefined, 'appointmentNo'],
|
||||
[customsCaseFieldGaps, { customsCase: { id: ids.id, customsNo: 'CUS-1', orderId: ids.order, inboundId: ids.inbound, containerBookingId: ids.container, checklistVersionId: ids.id, checklistCode: 'CHECK', checklistVersionNo: 'V1', status: 'DOCUMENTS_PENDING', updatedAt: at } }, undefined, 'checklistCode'],
|
||||
[outboundAggregateFieldGaps, { orderId: ids.order, inboundId: ids.inbound, inboundCode: 'IN-1', customerId: ids.id, customerName: '测试客户', containerBookingId: ids.container, containerNo: 'MSCU1234567', loadingPlanId: ids.plan, loadingPlanStatus: 'COMPLETED', pickTaskId: ids.task, pickTaskStatus: 'COMPLETED', oceanBookingId: ids.id, oceanBookingStatus: 'RELEASED', dispatchStatus: 'READY_FOR_DISPATCH', version: 1, gate: { items: [] } }, undefined, 'inboundCode'],
|
||||
]
|
||||
for (const [validator, sample, options, missing] of samples) {
|
||||
assert.deepEqual(validator(sample, options), [])
|
||||
const invalid = structuredClone(sample)
|
||||
const target = invalid.portSchedule?.schedule ?? invalid.customsCase ?? invalid
|
||||
target[missing] = ''
|
||||
assert.ok(validator(invalid, options).includes(missing))
|
||||
}
|
||||
})
|
||||
|
||||
test('字段断言接入正式生产模块和默认全量入口', () => {
|
||||
const consumers = {
|
||||
'phase3-container-loading.mjs': ['containerFieldGaps', 'preloadFieldGaps', 'loadingListFieldGaps'],
|
||||
'ocean-bookings.mjs': ['oceanBookingFieldGaps'],
|
||||
'phase3-port-schedules.mjs': ['portScheduleFieldGaps'],
|
||||
'truck-appointments.mjs': ['truckAppointmentFieldGaps'],
|
||||
'phase3-customs.mjs': ['customsCaseFieldGaps'],
|
||||
'phase3-outbound-dispatch.mjs': ['outboundAggregateFieldGaps'],
|
||||
}
|
||||
for (const [file, tokens] of Object.entries(consumers)) {
|
||||
const source = read(repo, 'scripts', 'admin-test-env-modules', file)
|
||||
for (const token of tokens) assert.match(source, new RegExp(token), `${file}: ${token}`)
|
||||
}
|
||||
const entry = read(repo, 'scripts', 'admin-test-env-acceptance.mjs')
|
||||
for (const module of Object.keys(consumers).map((file) => file.replace(/\.mjs$/, ''))) {
|
||||
assert.match(entry, new RegExp(`['"]${module}['"]`), `default entry: ${module}`)
|
||||
}
|
||||
})
|
||||
|
||||
test('矩阵登记 LANDED 与 UNKNOWN 且禁止 fixture 伪造未知字段', () => {
|
||||
const matrix = read(workspace, 'docs', '三期', '货柜履约原型字段验收矩阵-2026-08-17.md')
|
||||
assert.match(matrix, /LANDED/)
|
||||
assert.match(matrix, /UNKNOWN/)
|
||||
assert.match(matrix, /不得由 fixture 伪造|不生成假值/)
|
||||
for (const label of ['货柜', '预装清单', '装柜清单', '订舱', '船期', '拖车', '报关协同', '国内出仓']) {
|
||||
assert.match(matrix, new RegExp(label))
|
||||
}
|
||||
})
|
||||
@@ -2,6 +2,7 @@ const MODULE = 'ocean-bookings'
|
||||
const OPTIONS = { scenario: 'ocean-booking-lifecycle', deploymentBlocker: true }
|
||||
const BASE = '/api/platform/v1/ocean-bookings'
|
||||
import { oceanBookingDraft, oceanBookingDraftWithoutCustomer, resolveOceanBookingMaster } from './ocean-booking-fixture.mjs'
|
||||
import { oceanBookingFieldGaps } from './fulfillment-list-field-contract.mjs'
|
||||
|
||||
function id(value) { return value == null ? '' : String(value) }
|
||||
function codeOf(r) { return r?.body?.code == null ? 'missing' : String(r.body.code) }
|
||||
@@ -247,6 +248,9 @@ export async function run(context) {
|
||||
version = Number(releasedDetail?.oceanBooking?.version)
|
||||
const final = await request(context, 'GET', path); const fd = data(final)
|
||||
context.apiSuccess(MODULE, '回查 RELEASED 订舱详情', final, '', OPTIONS)
|
||||
const bookingFieldGaps = oceanBookingFieldGaps(fd?.oceanBooking)
|
||||
context.assert(MODULE, '订舱列表所需客户、船公司、代理、航次与关键时间字段来源完整', final,
|
||||
bookingFieldGaps.length === 0, `missing=${bookingFieldGaps.join(',') || 'none'}`, OPTIONS)
|
||||
context.assert(MODULE, '独立订舱状态历史、审计操作者和终态完整', final,
|
||||
fd?.oceanBooking?.status === 'RELEASED' && fd.histories?.length >= 4 && fd.histories.every((h) => h.operatorName)
|
||||
&& fd?.containers?.length === 2,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { publishPlatformLogisticsChannels } from './ocean-booking-fixture.mjs'
|
||||
import { diffPushPreloadStability, projectPushPreloadStability } from './push-preload-stability.mjs'
|
||||
import { containerFieldGaps, loadingListFieldGaps, preloadFieldGaps } from './fulfillment-list-field-contract.mjs'
|
||||
|
||||
const MODULE = 'phase3-container-loading'
|
||||
const OPTIONS = { scenario: 'phase3-container-smart-loading-pda', deploymentBlocker: true }
|
||||
@@ -431,6 +432,10 @@ export async function run(context) {
|
||||
activeRealRows.length === 1 && id(activeRealRows[0]?.id) === containerId
|
||||
&& !records(activeContainers).some((item) => id(item?.id) === id(requirement.containerBookingId)),
|
||||
`activeIds=${activeRealRows.map((item) => id(item?.id)).join('|') || 'none'}, mergedSourceId=${id(requirement.containerBookingId)}`, OPTIONS)
|
||||
const containerGaps = containerFieldGaps(activeRealRows[0], { requireContainerNo: true })
|
||||
context.assert(MODULE, '货柜列表关键展示字段来自正式柜次快照且格式完整', activeContainersResponse,
|
||||
activeRealRows.length === 1 && containerGaps.length === 0,
|
||||
`missing=${containerGaps.join(',') || 'none'}`, OPTIONS)
|
||||
|
||||
const mergedBookingResponse = await context.request('GET', `/api/platform/v1/ocean-bookings/${bookingId}`)
|
||||
const mergedBooking = context.apiSuccess(MODULE, '合并后回查订舱真实柜当前操作身份', mergedBookingResponse, '', OPTIONS)
|
||||
@@ -481,9 +486,13 @@ export async function run(context) {
|
||||
const preloadPageResponse = await context.request('GET',
|
||||
`${FIRST_LEG}/loading-plans?warehouseId=${encodeURIComponent(warehouseId)}&keyword=${encodeURIComponent(realContainerNo)}&page=1&size=20`)
|
||||
const preloadPage = context.apiSuccess(MODULE, '查询本轮预装清单', preloadPageResponse, '', OPTIONS)
|
||||
const preloadRow = records(preloadPage).find((item) => id(item.planId) === planId)
|
||||
context.assert(MODULE, '预装清单包含本轮已推送 EDITING 计划', preloadPageResponse,
|
||||
records(preloadPage).some((item) => id(item.planId) === planId && item.status === 'EDITING'),
|
||||
`records=${records(preloadPage).length}`, OPTIONS)
|
||||
const preloadGaps = preloadFieldGaps(preloadRow)
|
||||
context.assert(MODULE, '预装清单客户、仓库、箱型、数量重量体积与时间字段完整', preloadPageResponse,
|
||||
preloadGaps.length === 0, `missing=${preloadGaps.join(',') || 'none'}`, OPTIONS)
|
||||
|
||||
const unconfirmedTaskResponse = await context.request('GET',
|
||||
`${PDA}/tasks?loadingPlanId=${encodeURIComponent(planId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
@@ -784,9 +793,13 @@ export async function run(context) {
|
||||
const confirmedPageResponse = await context.request('GET',
|
||||
`${FIRST_LEG}/loading-lists?warehouseId=${encodeURIComponent(warehouseId)}&keyword=${encodeURIComponent(containerNo)}&page=1&size=20`)
|
||||
const confirmedPage = context.apiSuccess(MODULE, '查询完成装柜清单', confirmedPageResponse, '', OPTIONS)
|
||||
const confirmedRow = records(confirmedPage).find((item) => id(item.planId) === planId)
|
||||
context.assert(MODULE, '完成清单分页包含本轮计划', confirmedPageResponse,
|
||||
records(confirmedPage).some((item) => id(item.planId) === planId && item.status === 'COMPLETED'),
|
||||
`records=${records(confirmedPage).length}`, OPTIONS)
|
||||
const loadingListGaps = loadingListFieldGaps(confirmedRow)
|
||||
context.assert(MODULE, '装柜清单柜号、仓库、数量重量体积与时间字段完整', confirmedPageResponse,
|
||||
loadingListGaps.length === 0, `missing=${loadingListGaps.join(',') || 'none'}`, OPTIONS)
|
||||
|
||||
const confirmedDetailResponse = await context.request('GET',
|
||||
`${FIRST_LEG}/loading-lists/${planId}?warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { customsCaseFieldGaps } from './fulfillment-list-field-contract.mjs'
|
||||
|
||||
const MODULE = 'phase3-customs'
|
||||
const OPTIONS = { scenario: 'phase3-customs-collaboration', deploymentBlocker: true }
|
||||
const PLATFORM = '/api/platform/v1/customs'
|
||||
@@ -183,12 +185,15 @@ export async function run(context) {
|
||||
{ 'Idempotency-Key': key(context, 'case') })
|
||||
const created = context.apiSuccess(MODULE, '创建客户订单柜次关联报关协同单', caseResponse, '', OPTIONS)
|
||||
const caseId = id(created?.customsCase?.id)
|
||||
const customsFieldGaps = customsCaseFieldGaps(created)
|
||||
context.assert(MODULE, '报关单绑定本轮实际承载来源分片、入库单、柜次和清单版本', caseResponse,
|
||||
Boolean(caseId) && id(created?.customsCase?.tenantId) === tenantId && id(created?.customsCase?.orderId) === actualOrderId
|
||||
&& id(created?.customsCase?.inboundId) === inboundId && id(created?.customsCase?.containerBookingId) === containerBookingId
|
||||
&& id(created?.customsCase?.checklistVersionId) === checklistVersionId
|
||||
&& created?.customsCase?.status === 'DOCUMENTS_PENDING' && Number(created?.customsCase?.version) === 0,
|
||||
`caseId=${caseId || 'missing'}, parentOrderId=${parentOrderId}, sourceOrderId=${sourceOrderId}, actualOrderId=${actualOrderId}, status=${created?.customsCase?.status || 'missing'}`, OPTIONS)
|
||||
context.assert(MODULE, '报关列表单号、订单入库柜次、清单版本、状态与更新时间字段完整', caseResponse,
|
||||
customsFieldGaps.length === 0, `missing=${customsFieldGaps.join(',') || 'none'}`, OPTIONS)
|
||||
if (!caseId) return
|
||||
|
||||
const caseReplay = await request(context, 'POST', `${PLATFORM}/cases`, casePayload, context.token,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { outboundAggregateFieldGaps } from './fulfillment-list-field-contract.mjs'
|
||||
|
||||
const MODULE = 'phase3-outbound-dispatch'
|
||||
const OPTIONS = { scenario: 'phase3-domestic-outbound-gate', deploymentBlocker: true }
|
||||
const BASE = '/api/platform/v1/first-leg/outbound-dispatches'
|
||||
@@ -43,10 +45,13 @@ export async function run(context) {
|
||||
const aggregates = context.apiSuccess(MODULE, '按入库单和柜次查询出仓主聚合', listResponse, selector, OPTIONS)
|
||||
const aggregate = Array.isArray(aggregates) ? aggregates.find((item) => id(item.inboundId) === inboundId
|
||||
&& id(item.containerBookingId) === containerBookingId) : null
|
||||
const outboundFieldGaps = outboundAggregateFieldGaps(aggregate)
|
||||
context.assert(MODULE, '主聚合串联订单、入库单、柜次、计划与客户隔离键', listResponse,
|
||||
Boolean(aggregate?.orderId) && Boolean(aggregate?.tenantId) && Boolean(aggregate?.loadingPlanId)
|
||||
&& id(aggregate?.inboundId) === inboundId && id(aggregate?.containerBookingId) === containerBookingId,
|
||||
`orderId=${id(aggregate?.orderId) || 'missing'}, tenantId=${id(aggregate?.tenantId) || 'missing'}, planId=${id(aggregate?.loadingPlanId) || 'missing'}`, OPTIONS)
|
||||
context.assert(MODULE, '国内出仓列表客户、订单入库、柜次计划、PDA订舱与门禁摘要完整', listResponse,
|
||||
outboundFieldGaps.length === 0, `missing=${outboundFieldGaps.join(',') || 'none'}`, OPTIONS)
|
||||
if (!aggregate) return
|
||||
|
||||
const orderResponse = await request(context, 'GET', `${BASE}?orderId=${encodeURIComponent(id(aggregate.orderId))}`)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { portScheduleFieldGaps } from './fulfillment-list-field-contract.mjs'
|
||||
|
||||
const MODULE = 'phase3-port-schedules'
|
||||
const OPTIONS = { scenario: 'port-schedule-source-lifecycle', deploymentBlocker: true }
|
||||
const BASE = '/api/platform/v1/port-schedules'
|
||||
@@ -135,6 +137,7 @@ export async function run(context) {
|
||||
const createResponse = await request(context, 'POST', BASE, payload, { 'Idempotency-Key': createKey })
|
||||
const created = context.apiSuccess(MODULE, '新增独立港务船期观测', createResponse, '', OPTIONS)
|
||||
const scheduleId = id(created?.portSchedule?.schedule?.id)
|
||||
const scheduleFieldGaps = portScheduleFieldGaps(created)
|
||||
context.assert(MODULE, '船期来源、时效、截单截关截港和关联字段完整', createResponse,
|
||||
Boolean(scheduleId) && created?.portSchedule?.expired === false
|
||||
&& created?.portSchedule?.schedule?.sourceType === 'CARRIER'
|
||||
@@ -146,6 +149,8 @@ export async function run(context) {
|
||||
&& Boolean(created?.portSchedule?.schedule?.customsCutoffAt)
|
||||
&& Boolean(created?.portSchedule?.schedule?.portCutoffAt),
|
||||
`scheduleId=${scheduleId || 'missing'}, expired=${created?.portSchedule?.expired ?? 'missing'}`, OPTIONS)
|
||||
context.assert(MODULE, '船期列表来源、船名航次、港口与关键时间字段格式完整', createResponse,
|
||||
scheduleFieldGaps.length === 0, `missing=${scheduleFieldGaps.join(',') || 'none'}`, OPTIONS)
|
||||
if (!scheduleId) return
|
||||
|
||||
const replay = await request(context, 'POST', BASE, payload, { 'Idempotency-Key': createKey })
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { truckAppointmentFieldGaps } from './fulfillment-list-field-contract.mjs'
|
||||
|
||||
const MODULE = 'truck-appointments'
|
||||
const OPTIONS = { scenario: 'truck-appointment-lifecycle', deploymentBlocker: true }
|
||||
const BASE = '/api/platform/v1/truck-appointments'
|
||||
@@ -189,6 +191,9 @@ export async function run(context) {
|
||||
const action = async (name, endpoint, expectedStatus) => { const response = await request(context, 'POST', `${path}/${endpoint}`, { expectedVersion: version, reason: context.marker }, { 'Idempotency-Key': `${context.marker}-${endpoint}` }); const result = context.apiSuccess(MODULE, name, response, '', OPTIONS); context.assert(MODULE, `${name} 状态返回`, response, result?.status === expectedStatus, `status=${result?.status || 'missing'}`, OPTIONS); version = Number(result?.version) }
|
||||
for (const [name, endpoint, status] of [['提交预约','submit','PENDING_DISPATCH'],['派车','dispatch','DISPATCHED'],['开始提货','pickup','PICKUP_IN_PROGRESS'],['到仓','arrive','ARRIVED_WAREHOUSE'],['开始装货','load','LOADING'],['离场','depart','DEPARTED'],['完成','complete','COMPLETED']]) await action(name, endpoint, status)
|
||||
const final = await request(context, 'GET', path); const finalData = data(final); context.apiSuccess(MODULE, '回查 COMPLETED 拖车预约', final, '', OPTIONS)
|
||||
const appointmentFieldGaps = truckAppointmentFieldGaps(finalData)
|
||||
context.assert(MODULE, '拖车列表客户、仓库、订舱柜次、供应商与关键时间字段完整', final,
|
||||
appointmentFieldGaps.length === 0, `missing=${appointmentFieldGaps.join(',') || 'none'}`, OPTIONS)
|
||||
context.assert(MODULE, '拖车预约状态终态和费用快照正确', final, finalData?.status === 'COMPLETED' && finalData?.payableAmount != null && finalData?.receivableAmount != null, `status=${finalData?.status || 'missing'}`, OPTIONS)
|
||||
const history = await request(context, 'GET', `${path}/history`); const histories = context.apiSuccess(MODULE, '查询拖车预约状态历史', history, '', OPTIONS)
|
||||
const actions = Array.isArray(histories) ? histories.map((item) => item.action) : []
|
||||
|
||||
Reference in New Issue
Block a user