feat(dispatch): unify whole vehicle scheduling, source ownership and staged execution
This commit is contained in:
@@ -5,8 +5,9 @@ import { createHash, randomUUID } from 'node:crypto'
|
||||
import { AcceptanceRun } from './admin-test-env-acceptance.mjs'
|
||||
import { parseCrossCustomerArguments, runCrossCustomer } from './fleet-cross-customer-test-env.mjs'
|
||||
import { guardFleetWrites, redactFleetWrite, findFleetActorPhone } from './fleet-test-env-acceptance.mjs'
|
||||
import { controlledPalletMeasurements } from './admin-test-env-modules/fleet-trip-fulfillment.mjs'
|
||||
import { pickAll, saveReceiptAndComplete } from './admin-test-env-modules/phase5-overseas-fulfillment.mjs'
|
||||
import { confirmFleetLoading } from './admin-test-env-modules/fleet-trip-fulfillment.mjs'
|
||||
import { testPlannedCapacity } from './admin-test-env-modules/fleet-booking-reservation-acceptance.mjs'
|
||||
import { pickAll, saveReceiptAndComplete, arrangeDispatchDelivery } from './admin-test-env-modules/phase5-overseas-fulfillment.mjs'
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const API = '/api/platform/v1/last-leg', POOL = `${API}/fleet/capacity-offers`, IAM = '/api/platform/v1/iam'
|
||||
@@ -20,7 +21,6 @@ export function poolLocalDateTime(date) {
|
||||
requireThat(date instanceof Date && Number.isFinite(date.getTime()), 'VALID_LOCAL_DATE_REQUIRED')
|
||||
return new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().slice(0, 19)
|
||||
}
|
||||
const localTime = ms => poolLocalDateTime(new Date(Date.now() + ms))
|
||||
const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aH1kAAAAASUVORK5CYII=', 'base64')
|
||||
|
||||
export const POOL_COVERAGE = Object.freeze([
|
||||
@@ -271,9 +271,11 @@ async function verifyPauseAndCutoff({ offerNo, bookingNo, salesToken, dispatcher
|
||||
async function poolScenario(context, fixture, { execute = false, readInventory, credentials }) {
|
||||
if (!execute) return poolPlan({ execute: false, runId: 'plan' })
|
||||
requireThat(fixture?.marker === context.marker && fixture.members?.length === 2 && fixture.pdaToken
|
||||
&& fixture.dispatchNo && fixture.tripProof?.dispatchNo === fixture.dispatchNo
|
||||
&& fixture.members.every(member => member.dispatchNo === fixture.dispatchNo)
|
||||
&& typeof readInventory === 'function', 'CURRENT_PHYSICAL_FIXTURE_REQUIRED')
|
||||
const proof = context.createdResources.fleetCapacityPool = { status: 'CHECKING', marker: context.marker,
|
||||
tripNo: fixture.tripNo, covered: [], remaining: [...POOL_REMAINING], actors: [], orders: [], snapshots: {} }
|
||||
tripNo: fixture.tripNo, dispatchNo: fixture.dispatchNo, covered: [], remaining: [...POOL_REMAINING], actors: [], orders: [], snapshots: {} }
|
||||
const tripProof = context.createdResources.fleetTrips.find(row => row.tripNo === fixture.tripNo)
|
||||
const tripPath = `${API}/fleet/trips/${encode(fixture.tripNo)}`
|
||||
const key = label => createHash('sha256').update(`${context.marker}:POOL:${label}`).digest('hex')
|
||||
@@ -315,7 +317,7 @@ async function poolScenario(context, fixture, { execute = false, readInventory,
|
||||
for (let n = 0; n < 2; n++) dispatchers.push(await createActor('dispatch', n, [PREFIX + 'manage']))
|
||||
const fulfillmentToken = await createActor('fulfillment', 0, [PREFIX + 'manage',
|
||||
'platform:fleet:trip:booking-edit', 'platform:fleet:trip:booking-confirm', 'platform:fleet:trip:edit',
|
||||
'platform:overseas-warehouse:inbound-order:smart-dispatch'])
|
||||
'platform:overseas-warehouse:dispatch:create'])
|
||||
for (const member of fixture.members) {
|
||||
const credential = credentials?.get(member.customerId)
|
||||
check('Original fixture customer credentials available only in memory', Boolean(credential))
|
||||
@@ -324,15 +326,23 @@ async function poolScenario(context, fixture, { execute = false, readInventory,
|
||||
await reject('Merchant token cannot read internal capacity pool', 'GET', `${POOL}?page=1&size=20`, undefined, login.accessToken)
|
||||
}
|
||||
proof.covered.push('merchant-cannot-read-pool')
|
||||
const dispatchPath = `${API}/dispatches/${encode(fixture.dispatchNo)}`
|
||||
let main = await request('GET', dispatchPath)
|
||||
check('One native main retains both original unpicked customers', 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: key('RETURN-BEFORE-POOL'), reason: 'TEST零扫码显式退回以追加本轮外接实收' })
|
||||
main = await request('GET', dispatchPath)
|
||||
check('Explicit return preserves main identity and all sources before pool changes', main.dispatchNo === fixture.dispatchNo && main.stage === 'BEFORE_LOADING' && main.reviewState === 'DRAFT')
|
||||
let detail = await request('GET', tripPath), plan = await request('GET', `${tripPath}/capacity-plan`)
|
||||
check('One actual thirty-space trip and two original real pallets', Number(detail.trip.maxPalletSpaces) === 30 && plan.cargo.length === 2)
|
||||
const planned = controlledPalletMeasurements(plan.cargo, detail.trip.stops).map(p => ({ ...p, palletSpaces: 9 }))
|
||||
const planned = testPlannedCapacity(plan.cargo, detail.trip.stops).map(p => ({ ...p, palletSpaces: 9 }))
|
||||
await request('POST', `${tripPath}/capacity-plan`, { recordVersion: plan.recordVersion, manifestHash: plan.manifestHash, pallets: planned, reason: 'TEST容量计划18托位;实际装载另行实测确认' })
|
||||
detail = await request('GET', tripPath)
|
||||
const offerDraft = await request('POST', POOL, { tripNo: fixture.tripNo, tripRecordVersion: detail.trip.recordVersion,
|
||||
cutoffAt: new Date(Date.now() + 1800000).toISOString(), contactName: 'TEST调度', contactPhone: '19900000000', remark: context.marker, idempotencyKey: key('OFFER') }, dispatchers[0])
|
||||
let offer = (await request('POST', `${POOL}/${offerDraft.offer.offerNo}/publish`, { tripRecordVersion: offerDraft.offer.tripRecordVersion,
|
||||
offerRecordVersion: offerDraft.offer.recordVersion, idempotencyKey: key('PUBLISH') }, dispatchers[0])).offer
|
||||
proof.planEvidence = { actual: false, reason: 'TEST销售占位计划18/30,不能作为实载回执', pallets: planned }
|
||||
proof.offerNo = offer.offerNo
|
||||
const destinationKey = plan.cargo[0].destinationKey
|
||||
check('Owned physical sources share exact destination', plan.cargo.every(row => row.destinationKey === destinationKey))
|
||||
@@ -458,33 +468,49 @@ async function poolScenario(context, fixture, { execute = false, readInventory,
|
||||
check('Sales retains its own history after close', history.records.some(row => row.bookingNo === first.bookingNo)
|
||||
&& history.records.every(row => row.salesOwnerUserId === first.salesOwnerUserId))
|
||||
proof.covered.push('close-and-successful-confirmation-replay')
|
||||
detail = await request('GET', tripPath)
|
||||
for (const binding of detail.dispatches) detail = await request('POST', `${tripPath}/dispatches/${encode(binding.dispatchNo)}/review`, { recordVersion: detail.trip.recordVersion, decision: 'APPROVED' })
|
||||
main = await request('GET', dispatchPath)
|
||||
check('All appended physical pallets remain on the original one native main before review', main.dispatchNo === fixture.dispatchNo
|
||||
&& main.stage === 'BEFORE_LOADING' && Number(main.counts?.packageCount) === 4 && Number(main.counts?.pickedPackageCount) === 0)
|
||||
let reviewed = main
|
||||
for (const action of ['submit-review', 'dispatch-review', 'loading-review']) reviewed = await request('POST', `${dispatchPath}/${action}`,
|
||||
{ recordVersion: reviewed.recordVersion, idempotencyKey: key(`POOL-${action}`),
|
||||
...(action === 'submit-review' ? { remark: context.marker } : { decision: 'APPROVED', reason: context.marker }) })
|
||||
const members = [...fixture.members, ...poolMembers]
|
||||
check('Original and pool receipts all belong to the same trip main', members.every(member => member.dispatchNo === fixture.dispatchNo))
|
||||
proof.pickScopes = []
|
||||
const namespace = await pickPoolDispatch(context, fixture.pdaToken, fixture.members[0])
|
||||
proof.pickScopes.push({ dispatchNo: fixture.dispatchNo, customerIds: [...new Set(members.map(row => row.customerId))], namespace })
|
||||
const task = await request('GET', `/api/pda/v1/last-leg/dispatches/${encode(fixture.dispatchNo)}`, undefined, fixture.pdaToken)
|
||||
check('All four owned and external pallets were really picked within one native main', task.status === 'ACTIVE' && task.stage === 'LOADING'
|
||||
&& task.pickingReconciled === true && Number(task.counts?.packageCount) === 4 && Number(task.counts?.pickedPackageCount) === 4
|
||||
&& task.items.length === 4 && members.every(member => task.items.some(row => id(row.customerId) === member.customerId
|
||||
&& row.palletNo === member.palletNo && Number(row.pickedPackages) === 1)))
|
||||
await confirmFleetLoading(context, MODULE, tripProof, fixture.pdaToken, fixture.loadingMeasurements)
|
||||
const arranged = await arrangeDispatchDelivery(context, fixture.dispatchNo)
|
||||
check('One native platform arrangement covers all four real sources and both customers', arranged?.deliveries?.length === 2
|
||||
&& arranged.outbounds?.length === 2 && arranged.deliveries.reduce((sum, row) => sum + Number(row.packageCount), 0) === 4)
|
||||
for (const member of members) {
|
||||
const namespace = await pickPoolDispatch(context, fixture.pdaToken, member)
|
||||
proof.pickScopes.push({ customerId: member.customerId, dispatchNo: member.dispatchNo, namespace })
|
||||
const task = await request('GET', `/api/pda/v1/last-leg/dispatches/${encode(member.dispatchNo)}`, undefined, fixture.pdaToken)
|
||||
check('Every owned and external pallet was actually picked', task.items.every(row => Number(row.pickedPackages) === Number(row.scheduledPackages)))
|
||||
const matches = arranged.deliveries.filter(row => id(row.customerId) === member.customerId)
|
||||
check('Every source maps to its exact customer delivery without invented child dispatches', matches.length === 1)
|
||||
Object.assign(member, { outboundNo: matches[0].outboundNo, deliveryNo: matches[0].deliveryNo })
|
||||
}
|
||||
const loading = await request('GET', `${tripPath}/loading`)
|
||||
check('All four original physical pallets present for actual loading', loading.cargo.length === 4 && !loading.blockReason)
|
||||
const loaded = await request('POST', `${tripPath}/loading`, { recordVersion: loading.recordVersion, manifestHash: loading.manifestHash,
|
||||
pallets: controlledPalletMeasurements(loading.cargo, detail.trip.stops), reason: 'TEST受控实测每实托1托位;不同于之前18托位接单计划' })
|
||||
detail = await request('POST', `${tripPath}/loading-review`, { recordVersion: loaded.recordVersion, decision: 'APPROVED' })
|
||||
const departBody = { recordVersion: detail.trip.recordVersion, idempotencyKey: key('DEPART'), remark: context.marker }
|
||||
const departPath = `/api/pda/v1/last-leg/dispatches/${encode(fixture.members[0].dispatchNo)}/depart`
|
||||
const readyTask = await request('GET', `/api/pda/v1/last-leg/dispatches/${encode(fixture.dispatchNo)}`, undefined, fixture.pdaToken)
|
||||
check('Arrangement leaves native main ready in AFTER_LOADING', readyTask.status === 'ACTIVE' && readyTask.stage === 'AFTER_LOADING' && readyTask.actions?.DEPART?.allowed === true)
|
||||
const departBody = { recordVersion: readyTask.recordVersion, idempotencyKey: key('DEPART'), remark: context.marker }
|
||||
const departPath = `/api/pda/v1/last-leg/dispatches/${encode(fixture.dispatchNo)}/depart`
|
||||
const departed = await request('POST', departPath, departBody, fixture.pdaToken)
|
||||
check('One whole-trip actual departure includes exactly four dispatches', departed.status === 'IN_TRANSIT' && Number(departed.shippedPackages) === 4
|
||||
&& departed.dispatches.length === 4 && members.every(member => departed.dispatches.some(row => row.dispatchNo === member.dispatchNo)))
|
||||
check('Departure original-key replay is explicit', (await request('POST', departPath, departBody, fixture.pdaToken)).replayed === true)
|
||||
for (const [index, member] of members.entries()) {
|
||||
const path = `${API}/delivery-orders/${encode(member.deliveryNo)}?customerId=${encode(member.customerId)}`
|
||||
const currentDelivery = await request('GET', path)
|
||||
await request('PUT', path.replace('?', '/status?'), { status: 'DELIVERING', expectedVersion: currentDelivery.order.version, remark: context.marker })
|
||||
const signed = await saveReceiptAndComplete(context, { deliveryNo: member.deliveryNo }, member.customerId, index)
|
||||
check('One native actual departure covers four sources and two customer deliveries', departed.dispatchNo === fixture.dispatchNo
|
||||
&& departed.status === 'ACTIVE' && departed.stage === 'DELIVERING' && Number(departed.result?.shippedPackages) === 4
|
||||
&& departed.result.deliveries.length === 2)
|
||||
const departReplay = await request('POST', departPath, departBody, fixture.pdaToken)
|
||||
check('Departure original-key replay preserves exact version and documents', departReplay.replayed === true
|
||||
&& id(departReplay.recordVersion) === id(departed.recordVersion) && JSON.stringify(departReplay.result) === JSON.stringify(departed.result))
|
||||
for (const [index, delivery] of arranged.deliveries.entries()) {
|
||||
const actual = await request('GET', `${API}/delivery-orders/${encode(delivery.deliveryNo)}?customerId=${encode(delivery.customerId)}`)
|
||||
check('Actual departure has already set the customer delivery to DELIVERING', actual.order.status === 'DELIVERING')
|
||||
const signed = await saveReceiptAndComplete(context, delivery, delivery.customerId, index)
|
||||
check('Receipt actually completes this delivery', signed?.order?.status === 'DELIVERED')
|
||||
check('Signing cannot create or duplicate agreed fees', JSON.stringify(actual.fees) === JSON.stringify(signed.fees))
|
||||
}
|
||||
proof.covered.push('shared-real-departure-and-signature')
|
||||
for (const [index, order] of proof.orders.entries()) {
|
||||
@@ -494,7 +520,14 @@ async function poolScenario(context, fixture, { execute = false, readInventory,
|
||||
&& commercial.summary.fleetArrangement?.fulfillmentStage === 'COMPLETED' && Number(commercial.summary.fleetArrangement.signedPackages) === 1)
|
||||
const member = poolMembers[index], actual = await request('GET', `${API}/delivery-orders/${member.deliveryNo}?customerId=${order.customerId}`)
|
||||
const fees = actual.fees.filter(row => row.feeType === 'DELIVERY_FEE' && row.feeDirection === 'RECEIVABLE')
|
||||
check('One agreed receivable remains, no order/receipt duplicate fee', fees.length === 1 && Number(fees[0].amount) === 30 && fees[0].currency === 'CNY')
|
||||
const sameCustomerOrders = poolMembers.filter(row => row.customerId === order.customerId)
|
||||
const externalFees = fees.filter(row => row.currency === 'CNY' && Number(row.amount) === 30)
|
||||
check('Exactly one agreed external receivable per original source plus the separately uncovered owned service',
|
||||
externalFees.length === sameCustomerOrders.length && fees.length === sameCustomerOrders.length + 1
|
||||
&& fees.filter(row => row.currency === 'USD' && Number(row.amount) === 10).length === 1
|
||||
&& new Set(fees.map(row => id(row.id))).size === fees.length
|
||||
&& sameCustomerOrders.every(source => actual.operations.filter(op => op.operationNode === 'CONFIRM_EXTERNAL_FREIGHT'
|
||||
&& id(op.result).includes(`外接预订 ${source.bookingNo};`)).length === 1))
|
||||
const merchant = merchantSessions.find(session => session.customerId === order.customerId)
|
||||
const merchantPath = `/api/merchant/v1/last-leg/${order.businessType === 'US_TRUCK' ? 'us-truck-orders' : 'local-looseboard-orders'}/${encode(order.businessOrderNo)}`
|
||||
const own = await request('GET', merchantPath, undefined, merchant.accessToken)
|
||||
@@ -571,9 +604,10 @@ async function receiveAndBindPoolOrder(context, fixture, order, index, { request
|
||||
const inventory = await readInventory({ marker: context.marker, customerId: order.customerId, warehouseId: fixture.warehouseId })
|
||||
check('Immutable actual incoming STOCK_IN is unique', inventory.scope?.coverage === 'CUSTOMER_WAREHOUSE_COMPLETE'
|
||||
&& inventory.ledger.filter(row => row.businessOrderNo === incomingNo && row.businessType === 'STOCK_IN' && Number(row.deltaPackages) === 1).length === 1)
|
||||
const dispatchBase = `${API}/dispatches`, candidates = await request('GET', `${dispatchBase}/candidates?${new URLSearchParams({ sourceStage: 'RECEIVED', orderNo: incomingNo })}`)
|
||||
check('Received candidate exactly belongs to this commercial incoming', candidates.length === 1 && candidates[0].inboundCode === incomingNo
|
||||
&& id(candidates[0].customerId) === order.customerId && id(candidates[0].warehousePalletId) === id(pallet.warehousePalletId))
|
||||
const dispatchBase = `${API}/dispatches`, candidates = await request('GET', `${dispatchBase}/candidates?${new URLSearchParams({ warehouseId: fixture.warehouseId, orderNo: incomingNo })}`)
|
||||
check('Received native candidate exactly belongs to this commercial incoming', candidates.complete === true && candidates.candidates.length === 1
|
||||
&& candidates.candidates[0].inboundCode === incomingNo && id(candidates.candidates[0].customerId) === order.customerId
|
||||
&& candidates.candidates[0].palletNo === pallet.palletNo && candidates.candidates[0].selection?.sourceType === 'INBOUND_PLAN')
|
||||
bookingList = await request('GET', `${tripPath}/bookings`); booking = bookingList.records.find(row => row.bookingNo === order.bookingNo)
|
||||
const arrangeBody = { tripRecordVersion: bookingList.tripRecordVersion, recordVersion: booking.recordVersion,
|
||||
idempotencyKey: key(`ARRANGE-DISPATCH-${index}`) }
|
||||
@@ -581,13 +615,13 @@ async function receiveAndBindPoolOrder(context, fixture, order, index, { request
|
||||
const arranged = await request('POST', `${poolPath}/arrange-dispatch`, arrangeBody, fulfillmentToken)
|
||||
const arrangeReplay = await request('POST', `${poolPath}/arrange-dispatch`, arrangeBody, fulfillmentToken)
|
||||
check('One command generates and binds the actual receipt without a manually supplied number', arranged.booking.status === 'BOUND'
|
||||
&& arranged.booking.dispatchNo && arranged.booking.incomingNo === incomingNo && arranged.booking.customerId === order.customerId)
|
||||
&& arranged.booking.dispatchNo === fixture.dispatchNo && arranged.booking.incomingNo === incomingNo && arranged.booking.customerId === order.customerId)
|
||||
check('Arrange replay returns the original dispatch and version', arrangeReplay.replayed === true
|
||||
&& arrangeReplay.booking.dispatchNo === arranged.booking.dispatchNo && arrangeReplay.tripRecordVersion === arranged.tripRecordVersion)
|
||||
const dispatch = { dispatchNo: arranged.booking.dispatchNo }
|
||||
if (index === 0) proof.covered.push('pool-fulfillment-explicit-write-permissions', 'pool-arrange-dispatch-without-manual-number')
|
||||
let plan = await request('GET', `${tripPath}/capacity-plan`), detail = await request('GET', tripPath)
|
||||
const actualMeasurements = controlledPalletMeasurements(plan.cargo, detail.trip.stops)
|
||||
const plannedMeasurements = testPlannedCapacity(plan.cargo, detail.trip.stops)
|
||||
const restored = await verifyPoolBindingRevalidation({ offerDetail, check,
|
||||
blockedConfirm: async label => {
|
||||
const before = await latest(proof.revalidationDraftNo)
|
||||
@@ -597,7 +631,7 @@ async function receiveAndBindPoolOrder(context, fixture, order, index, { request
|
||||
check('Revalidation refusal keeps competing draft unchanged', inquiryFact(before) === inquiryFact(await latest(before.bookingNo)))
|
||||
},
|
||||
replan: () => request('POST', `${tripPath}/capacity-plan`, { recordVersion: plan.recordVersion, manifestHash: plan.manifestHash,
|
||||
pallets: actualMeasurements, reason: `${context.marker} 实收后重新核对,不重复扣外接预留` }),
|
||||
pallets: plannedMeasurements, reason: `${context.marker} 实收后重新核对,不重复扣外接预留` }),
|
||||
resume: async offer => (await request('POST', `${POOL}/${proof.offerNo}/resume`, { tripRecordVersion: offer.tripRecordVersion,
|
||||
offerRecordVersion: offer.recordVersion, idempotencyKey: key(`RESUME-AFTER-BIND-${index}`), reason: 'TEST实际绑定核量后显式恢复接单' }, dispatcherToken)).offer })
|
||||
const bound = await latest(order.bookingNo)
|
||||
@@ -605,34 +639,27 @@ async function receiveAndBindPoolOrder(context, fixture, order, index, { request
|
||||
const allBookings = await request('GET', `${tripPath}/bookings`)
|
||||
const load = rows => rows.reduce((total, row) => { for (const dimension of ['palletSpaces', 'weightKg', 'volumeCbm']) total[dimension] += Number(row[dimension]); return total },
|
||||
{ palletSpaces: 0, weightKg: 0, volumeCbm: 0 })
|
||||
const plannedLoad = load(actualMeasurements)
|
||||
const plannedLoad = load(plannedMeasurements)
|
||||
const reservedLoad = load(allBookings.records.filter(row => ['CONFIRMED', 'RECEIVED'].includes(row.status)).map(row => row.cargo))
|
||||
const capacity = { palletSpaces: detail.trip.maxPalletSpaces, weightKg: detail.trip.maxLoadKg, volumeCbm: detail.trip.maxVolumeCbm }
|
||||
assertPoolBoundCapacity(restored, destinationKey, plannedLoad, reservedLoad, capacity)
|
||||
check('BOUND reservation is removed exactly once from all capacity dimensions', Number(plannedLoad.palletSpaces) === 3 + index
|
||||
&& Number(reservedLoad.palletSpaces) === (index === 0 ? 4 : 0))
|
||||
;(proof.bindingCapacity ||= []).push({ bookingNo: order.bookingNo, status: bound.status, planned: plannedLoad,
|
||||
;(proof.bindingCapacity ||= []).push({ bookingNo: order.bookingNo, status: bound.status, actual: false, planned: plannedLoad,
|
||||
reserved: reservedLoad, remaining: restored.destinations.find(row => row.destinationKey === destinationKey).remaining })
|
||||
const path = `${dispatchBase}/${encode(dispatch.dispatchNo)}`, freight = await request('GET', `${path}/external-freight`)
|
||||
check('The one pool commercial source owns the one agreed freight', freight.requiresDeliveryFee === false && freight.quotes.length === 1
|
||||
&& freight.quotes[0].bookingNo === order.bookingNo && Number(freight.quotes[0].amount) === 30)
|
||||
detail = await request('GET', tripPath)
|
||||
const transfer = { idempotencyKey: key(`TRANSFER-${index}`), plannedDeliveryAt: localTime(21600000), deliveryMethod: 'TRUCK_PALLET', pickupMethod: 'WAREHOUSE_PICKUP',
|
||||
loadingType: 'PALLET', vehicleType: 'TRUCK', vehicleSize: '30托', vehiclePlate: detail.trip.vehiclePlate, driverName: detail.trip.driverName,
|
||||
driverPhone: detail.trip.driverPhone, appointmentAccount: 'SELF', externalFreightHash: freight.snapshotHash, fees: [], remark: context.marker }
|
||||
const converted = await request('POST', `${path}/transfer-delivery`, transfer)
|
||||
check('Pool source produces exactly one outbound and delivery', converted.outboundNo && converted.deliveries.length === 1)
|
||||
const replay = await request('POST', `${path}/transfer-delivery`, transfer)
|
||||
check('Transfer replay reuses outbound and delivery', replay.replayed === true && replay.outboundNo === converted.outboundNo && replay.deliveries[0].deliveryNo === converted.deliveries[0].deliveryNo)
|
||||
const nativeItems = await request('GET', `${dispatchBase}/${encode(fixture.dispatchNo)}/items`)
|
||||
check('Received pool source is present in the one native main without early outbound/delivery', nativeItems.items.some(row => id(row.customerId) === order.customerId
|
||||
&& row.inboundCode === incomingNo && id(row.warehousePalletId) === id(pallet.warehousePalletId) && Number(row.pickedPackages) === 0))
|
||||
return { customerId: order.customerId, bookingNo: order.bookingNo, businessOrderNo: order.businessOrderNo, incomingNo, dispatchNo: dispatch.dispatchNo,
|
||||
outboundNo: converted.outboundNo, deliveryNo: converted.deliveries[0].deliveryNo, palletId: id(pallet.warehousePalletId), palletNo: pallet.palletNo }
|
||||
palletId: id(pallet.warehousePalletId), palletNo: pallet.palletNo }
|
||||
|
||||
}
|
||||
|
||||
export async function runPoolAcceptance(config, { env = process.env, outputRoot = ROOT, runner = runCrossCustomer } = {}) {
|
||||
if (!config.execute) return poolPlan(config)
|
||||
const credentials = new Map(); let restoreWrites
|
||||
try {
|
||||
const result = await runner(config, { env, outputRoot,
|
||||
const result = await runner(config, { env, outputRoot, verificationScope: 'CAPACITY_POOL',
|
||||
contextFactory: options => {
|
||||
const context = new AcceptanceRun(options), original = context.request
|
||||
context.request = async function (...args) {
|
||||
|
||||
Reference in New Issue
Block a user