feat(dispatch): unify whole vehicle scheduling, source ownership and staged execution
This commit is contained in:
@@ -7,7 +7,7 @@ import { createHash, randomUUID } from 'node:crypto'
|
||||
import { AcceptanceRun, parseArguments, validateExecutionOptions, buildAcceptanceSummary, createFleetPoolGrayOptions } from './admin-test-env-acceptance.mjs'
|
||||
import { parseCrossCustomerArguments } from './fleet-cross-customer-test-env.mjs'
|
||||
import { createFleetCrossCustomerFixture } from './admin-test-env-modules/fleet-cross-customer-fixture.mjs'
|
||||
import { controlledPalletMeasurements } from './admin-test-env-modules/fleet-trip-fulfillment.mjs'
|
||||
import { testPlannedCapacity } from './admin-test-env-modules/fleet-booking-reservation-acceptance.mjs'
|
||||
import { createTestEvidenceReaders } from './fba-core-regression-evidence.mjs'
|
||||
import { createGrayEvidenceReaders } from './gray-regression-evidence.mjs'
|
||||
import { guardFleetWrites, redactFleetWrite, findFleetActorPhone } from './fleet-test-env-acceptance.mjs'
|
||||
@@ -77,8 +77,10 @@ export function grayUiSchedule(now = Date.now()) {
|
||||
|
||||
export function applyGrayUiTransportWindow(method, path, body, schedule) {
|
||||
if (method !== 'POST' || !/^\/api\/platform\/v1\/last-leg\/dispatches\/[^/]+\/transfer-delivery$/.test(path)) return body
|
||||
demand(body && schedule?.plannedDeliveryAt, 'GRAY_UI_DELIVERY_WINDOW_REQUIRED')
|
||||
return { ...body, plannedDeliveryAt: schedule.plannedDeliveryAt }
|
||||
demand(body && Array.isArray(body.groups) && body.groups.length && body.groups.every(group => group?.groupKey)
|
||||
&& schedule?.plannedDeliveryAt && Number.isFinite(Date.parse(schedule.plannedDeliveryAt)), 'GRAY_UI_NATIVE_DELIVERY_WINDOW_REQUIRED')
|
||||
const plannedDeliveryAt = new Date(schedule.plannedDeliveryAt).toISOString()
|
||||
return { ...body, groups: body.groups.map(group => ({ ...group, plannedDeliveryAt })) }
|
||||
}
|
||||
|
||||
/** Private directory override is for isolated offline tests, never exposed as a CLI option. */
|
||||
@@ -228,25 +230,35 @@ async function runUiFixtureCore(config, { env = process.env, outputRoot = ROOT,
|
||||
destinationWarehouseId: config.destinationWarehouseId, pdaToken: picker.token, readInventory })
|
||||
save('fixture.json', fixture)
|
||||
const trip = context.createdResources.fleetTrips.find(row => row.tripNo === fixture.tripNo), tripPath = `${API}/fleet/trips/${encodeURIComponent(fixture.tripNo)}`
|
||||
check('Existing producer created exactly two untouched customer dispatches', fixture.members.length === 2 && trip)
|
||||
let detail = await request('GET', tripPath), plan = await request('GET', `${tripPath}/capacity-plan`)
|
||||
check('Owned fixture vehicle has actual 30-space capacity', Number(detail.trip.maxPalletSpaces) === 30 && plan.cargo.length === 2)
|
||||
// GRAY delivery ETA was set on the producer's real transfer command, so all four times stay ordered.
|
||||
detail = await request('PUT', `${tripPath}/plan`, { recordVersion: detail.trip.recordVersion,
|
||||
check('Existing producer created two untouched customer sources on exactly one native main', fixture.members.length === 2 && trip
|
||||
&& fixture.dispatchNo && fixture.members.every(member => member.dispatchNo === fixture.dispatchNo) && fixture.tripProof?.dispatchNo === fixture.dispatchNo)
|
||||
const dispatchPath = `${API}/dispatches/${encodeURIComponent(fixture.dispatchNo)}`
|
||||
let main = await request('GET', dispatchPath), detail = await request('GET', tripPath), plan = await request('GET', `${tripPath}/capacity-plan`)
|
||||
check('Owned fixture vehicle has actual 30-space capacity with two real source pallets', Number(detail.trip.maxPalletSpaces) === 30 && plan.cargo.length === 2
|
||||
&& main.status === 'ACTIVE' && main.stage === 'LOADING' && Number(main.counts?.pickedPackageCount) === 0 && Number(main.counts?.shippedPackageCount) === 0)
|
||||
await request('POST', `${dispatchPath}/return-to-review`, { recordVersion: main.recordVersion,
|
||||
idempotencyKey: createHash('sha256').update(`${context.marker}:UI:RETURN`).digest('hex'), reason: 'TEST零扫码明确退回以设置UI计划时窗' })
|
||||
main = await request('GET', dispatchPath)
|
||||
check('Native UI plan edits only the explicit unpicked BEFORE_LOADING main', main.stage === 'BEFORE_LOADING' && main.reviewState === 'DRAFT')
|
||||
const uiPlanBody = { ...fixture.tripProof.updateRequest, recordVersion: main.recordVersion,
|
||||
idempotencyKey: createHash('sha256').update(`${context.marker}:UI:PLAN`).digest('hex'),
|
||||
plannedDepartureAt: schedule?.plannedDepartureAt || new Date(Date.now() + 4 * 3600000).toISOString(),
|
||||
expectedDriverEndAt: schedule?.expectedDriverEndAt || detail.trip.expectedDriverEndAt,
|
||||
stops: detail.trip.stops, reason: gray ? '灰度UI验收:统一截止、发车、到站与司机结束时间,不执行发车' : 'UI验收窗口:四小时后计划发车,不执行发车' })
|
||||
if (gray) {
|
||||
check('GRAY trip departure and driver end follow the explicit UI window', Date.parse(detail.trip.plannedDepartureAt) === Date.parse(schedule.plannedDepartureAt)
|
||||
&& Date.parse(detail.trip.expectedDriverEndAt) === Date.parse(schedule.expectedDriverEndAt))
|
||||
for (const member of fixture.members) {
|
||||
const delivery = await request('GET', `${API}/delivery-orders/${member.deliveryNo}?customerId=${member.customerId}`)
|
||||
check('Original customer delivery ETA follows the same valid UI window', delivery.order.plannedDeliveryAt === schedule.plannedDeliveryAt)
|
||||
}
|
||||
}
|
||||
estimatedArrivalAt: schedule ? new Date(schedule.plannedDeliveryAt).toISOString() : null,
|
||||
driver: { mode: 'MASTER', driverId: fixture.tripProof.driverId,
|
||||
evidence: { expectedTaskEndAt: schedule?.expectedDriverEndAt || fixture.tripProof.expectedDriverEndAt } },
|
||||
remark: 'TEST仅UI验收计划,不执行实载发车' }
|
||||
let reviewed = await request('PUT', dispatchPath, uiPlanBody)
|
||||
for (const action of ['submit-review', 'dispatch-review', 'loading-review']) reviewed = await request('POST', `${dispatchPath}/${action}`,
|
||||
{ recordVersion: reviewed.recordVersion, idempotencyKey: createHash('sha256').update(`${context.marker}:UI:${action}`).digest('hex'),
|
||||
...(action === 'submit-review' ? { remark: context.marker } : { decision: 'APPROVED', reason: context.marker }) })
|
||||
detail = await request('GET', tripPath)
|
||||
if (gray) check('GRAY native trip departure and driver end follow the explicit UI window', Date.parse(detail.trip.plannedDepartureAt) === Date.parse(schedule.plannedDepartureAt)
|
||||
&& Date.parse(detail.trip.expectedDriverEndAt) === Date.parse(schedule.expectedDriverEndAt))
|
||||
plan = await request('GET', `${tripPath}/capacity-plan`)
|
||||
const plannedPallets = testPlannedCapacity(plan.cargo, detail.trip.stops).map(row => ({ ...row, palletSpaces: 9 }))
|
||||
await request('POST', `${tripPath}/capacity-plan`, { recordVersion: plan.recordVersion, manifestHash: plan.manifestHash,
|
||||
pallets: controlledPalletMeasurements(plan.cargo, detail.trip.stops).map(row => ({ ...row, palletSpaces: 9 })), reason: '仅UI验收:自有计划18/总30,不作为实际发车计量' })
|
||||
pallets: plannedPallets, reason: '仅UI验收:自有计划18/总30,不作为实际发车计量' })
|
||||
proof.planEvidence = { actual: false, pallets: plannedPallets, reason: 'TEST UI销售计划18/30,不是实载测量' }
|
||||
const permissions = await request('GET', `${IAM}/permissions`), actorTokens = []
|
||||
const key = suffix => createHash('sha256').update(`${context.marker}:UI:${suffix}`).digest('hex')
|
||||
for (const kind of ['SALES_A', 'SALES_B', 'MANAGER']) {
|
||||
@@ -275,7 +287,7 @@ async function runUiFixtureCore(config, { env = process.env, outputRoot = ROOT,
|
||||
offerRecordVersion: draft.offer.recordVersion, idempotencyKey: key('PUBLISH') }, actorTokens[2])).offer
|
||||
check('OPEN UI offer is available with exactly 12 planned spaces', offer.status === 'OPEN' && offer.canCreateInquiry && offer.canConfirm
|
||||
&& offer.destinations.length === 1 && Number(offer.destinations[0].remaining.palletSpaces) === 12)
|
||||
proof.offerNo = offer.offerNo; proof.tripNo = fixture.tripNo
|
||||
proof.offerNo = offer.offerNo; proof.tripNo = fixture.tripNo; proof.dispatchNo = fixture.dispatchNo
|
||||
for (let index = 0; index < draftCount; index++) {
|
||||
const owner = index < perSales ? 0 : 1, member = fixture.members[owner]
|
||||
const body = uiInquiryBody(offer, member.customerId, index, key(`DRAFT-${index}`), gray)
|
||||
@@ -314,12 +326,14 @@ async function runUiFixtureCore(config, { env = process.env, outputRoot = ROOT,
|
||||
check('UI fixture has not picked departed or signed cargo', detail.trip.status === 'PLANNED')
|
||||
for (const member of fixture.members) {
|
||||
const dispatch = await request('GET', `${API}/dispatches/${member.dispatchNo}`)
|
||||
check('Original real cargo remains unpicked', Number(dispatch.pickedPackageCount) === 0 && Number(dispatch.shippedPackageCount) === 0)
|
||||
check('Original real cargo remains unpicked', dispatch.dispatchNo === fixture.dispatchNo && dispatch.status === 'ACTIVE' && dispatch.stage === 'LOADING'
|
||||
&& Number(dispatch.counts?.pickedPackageCount) === 0 && Number(dispatch.counts?.shippedPackageCount) === 0
|
||||
&& dispatch.documents?.outbounds?.length === 0 && dispatch.documents?.deliveries?.length === 0)
|
||||
}
|
||||
const final = await request('GET', `${POOL}/${offer.offerNo}`, undefined, actorTokens[2])
|
||||
check(`UI handoff keeps publication OPEN and ${draftCount} drafts untouched`, final.offer.status === 'OPEN' && final.offer.canCreateInquiry && Number(final.inquiries.total) === draftCount)
|
||||
proof.status = `OPEN_AND_${draftCount}_DRAFTS_READY_FOR_UI`
|
||||
secretDocument.business = { offerNo: proof.offerNo, tripNo: fixture.tripNo, warehouseId: fixture.warehouseId,
|
||||
secretDocument.business = { offerNo: proof.offerNo, tripNo: fixture.tripNo, dispatchNo: fixture.dispatchNo, warehouseId: fixture.warehouseId,
|
||||
destinationWarehouseId: config.destinationWarehouseId, cutoffAt: final.offer.cutoffAt,
|
||||
plannedDepartureAt: final.offer.plannedDepartureAt, ...(gray ? { uiSchedule: schedule } : {}),
|
||||
customers: fixture.members.map(row => ({ customerId: row.customerId, incomingNo: row.incomingNo, dispatchNo: row.dispatchNo })) }
|
||||
|
||||
Reference in New Issue
Block a user