test(fleet): close pool cancellation capacity and isolation matrices
This commit is contained in:
@@ -30,6 +30,10 @@ export const POOL_COVERAGE = Object.freeze([
|
||||
'merchant-cannot-read-pool', 'old-manual-order-endpoints-rejected',
|
||||
'assigned-vehicle-disable-and-shrink-rejected', 'paused-create-edit-confirm-rejected',
|
||||
'expired-cutoff-create-edit-confirm-rejected', 'weight-and-volume-independent-capacity-rejections',
|
||||
'confirmed-cancel-before-incoming-releases-capacity-and-replays', 'incoming-cancel-rejected-with-source-unchanged',
|
||||
'receipt-overcapacity-rejected-with-source-unchanged', 'open-offer-bind-replan-requires-explicit-resume',
|
||||
'bound-cargo-reservation-removed-without-double-count', 'other-sales-write-and-sensitive-trip-denied',
|
||||
'other-customer-commercial-read-denied-with-source-unchanged',
|
||||
])
|
||||
export const POOL_REMAINING = Object.freeze([
|
||||
'out-of-band-assigned-vehicle-drift-not-constructible-through-supported-api',
|
||||
@@ -40,7 +44,7 @@ export function parsePoolArguments(args) { return parseCrossCustomerArguments(ar
|
||||
export function poolPlan(config) {
|
||||
return { environment: 'TEST', execute: Boolean(config.execute), runId: config.runId,
|
||||
output: `reports/fleet-cross-${config.runId}`, coveredByThisEntry: POOL_COVERAGE, remainingSeparateAcceptance: POOL_REMAINING,
|
||||
sample: 'Two fresh qualified customers and owned one-box pallets, one 30-space trip, two pool business orders, one real incoming/pallet each. Planning uses 18 spaces; actual physical load is separately confirmed.',
|
||||
sample: 'Two fresh qualified customers and owned one-box pallets, one 30-space trip. Each pool business type has one safely cancelled pre-incoming sample and one fulfilled order with one real incoming/pallet. Planning uses 18 spaces; actual physical load is separately confirmed.',
|
||||
prerequisites: ['explicit TEST private env with CHONGCHONG_ENV=test and CHONGCHONG_ADMIN_*', 'current Warehouse/FBA IDs',
|
||||
'TEST read-only inventory evidence credentials', 'current capacity-offer permission seeds and backend deployed'],
|
||||
boundary: 'Default plan performs no network or filesystem writes. Execute retains every sample and original command, blocks all further writes on an unknown outcome, never deploys or writes business SQL. No automatic rerun or resume.' }
|
||||
@@ -58,6 +62,79 @@ export function assertSingleCommercial(before, after) {
|
||||
&& before.customerId === after.customerId, 'COMMERCIAL_REPLAY_IDENTITY_CHANGED')
|
||||
}
|
||||
|
||||
export function poolInquiryWrite(inquiry, idempotencyKey) {
|
||||
return { tripRecordVersion: inquiry.tripRecordVersion, offerRecordVersion: inquiry.offerRecordVersion,
|
||||
recordVersion: inquiry.recordVersion, customerId: inquiry.customerId, businessType: inquiry.businessType,
|
||||
deliveryMode: inquiry.deliveryMode, estimatedArrivalAt: inquiry.estimatedArrivalAt,
|
||||
expectedPalletCount: inquiry.expectedPalletCount, cargo: inquiry.cargo, quoteRef: null, idempotencyKey }
|
||||
}
|
||||
|
||||
export function poolReadIsDenied(response) {
|
||||
return ([403, 404].includes(response?.http) || response?.http === 200 && ['A0403', 'A0404'].includes(id(response.body?.code)))
|
||||
&& response.body?.data == null
|
||||
}
|
||||
|
||||
/** This fixture has exactly one shared unloading segment; verify all dimensions, not only a badge. */
|
||||
export function assertPoolBoundCapacity(offer, destinationKey, planned, reserved, capacity) {
|
||||
requireThat(offer?.status === 'OPEN' && offer.capacityKnown && offer.canConfirm && offer.segments?.length === 1,
|
||||
'OPEN_KNOWN_SINGLE_SEGMENT_REQUIRED')
|
||||
const segment = offer.segments[0]
|
||||
requireThat(segment.fromIndex === 0 && segment.toIndex === 1, 'EXACT_SHARED_SEGMENT_REQUIRED')
|
||||
for (const dimension of ['palletSpaces', 'weightKg', 'volumeCbm']) {
|
||||
const expected = Number(capacity[dimension]) - Number(planned[dimension]) - Number(reserved[dimension])
|
||||
requireThat(Number.isFinite(expected) && Number(segment.planned?.[dimension]) === Number(planned[dimension])
|
||||
&& Number(segment.reserved?.[dimension]) === Number(reserved[dimension])
|
||||
&& Number(segment.remaining?.[dimension]) === expected, `BOUND_LOAD_DOUBLE_COUNT_OR_MISSING_${dimension}`)
|
||||
const destinations = offer.destinations?.filter(row => row.destinationKey === destinationKey)
|
||||
requireThat(destinations?.length === 1 && Number(destinations[0].remaining?.[dimension]) === expected,
|
||||
`BOUND_DESTINATION_REMAINING_MISMATCH_${dimension}`)
|
||||
}
|
||||
}
|
||||
|
||||
export async function verifyPoolConfirmedCancellation({ inquiry, destinationKey, request, check, latest, command,
|
||||
businessPath, dispatcherToken, label }) {
|
||||
const path = `${POOL}/inquiries/${inquiry.bookingNo}`
|
||||
const confirmed = await request('POST', `${path}/confirm`, command(await latest(inquiry.bookingNo), `${label}-CONFIRM`), dispatcherToken)
|
||||
check('Cancellation sample really reserves one space', confirmed.inquiry.status === 'CONFIRMED' && confirmed.inquiry.businessOrderNo)
|
||||
assertPoolRemaining(confirmed.offer, destinationKey, 11)
|
||||
const before = await request('GET', businessPath(confirmed.inquiry))
|
||||
check('Confirmed cancellation sample has no incoming', before.summary.status === 'WAITING_INBOUND'
|
||||
&& before.summary.fleetArrangement?.bookingNo === inquiry.bookingNo && !before.summary.fleetArrangement.incomingNo)
|
||||
const cancelBody = { ...command(await latest(inquiry.bookingNo), `${label}-CANCEL`), reason: 'TEST未登记来货取消并释放承诺' }
|
||||
const cancelled = await request('POST', `${path}/cancel`, cancelBody, dispatcherToken)
|
||||
assertSingleCommercial(confirmed.inquiry, cancelled.inquiry)
|
||||
check('Confirmed booking is cancelled before incoming', cancelled.inquiry.status === 'CANCELLED' && !cancelled.inquiry.incomingNo)
|
||||
assertPoolRemaining(cancelled.offer, destinationKey, 12)
|
||||
const commercial = await request('GET', businessPath(cancelled.inquiry))
|
||||
check('Commercial cancellation is atomic with reservation release', commercial.summary.status === 'CANCELLED'
|
||||
&& commercial.summary.fleetArrangement?.bookingNo === inquiry.bookingNo && !commercial.summary.fleetArrangement.incomingNo)
|
||||
const replay = await request('POST', `${path}/cancel`, cancelBody, dispatcherToken)
|
||||
check('Original cancellation command replays without changing source', replay.replayed === true
|
||||
&& inquiryFact(replay.inquiry) === inquiryFact(cancelled.inquiry))
|
||||
check('Cancel replay keeps persisted commercial and booking facts unchanged',
|
||||
JSON.stringify(commercial) === JSON.stringify(await request('GET', businessPath(cancelled.inquiry)))
|
||||
&& inquiryFact(await latest(inquiry.bookingNo)) === inquiryFact(cancelled.inquiry))
|
||||
const current = await request('GET', `${POOL}/${inquiry.offerNo}`, undefined, dispatcherToken)
|
||||
assertPoolRemaining(current.offer, destinationKey, 12)
|
||||
return { bookingNo: inquiry.bookingNo, businessOrderNo: cancelled.inquiry.businessOrderNo, businessType: inquiry.businessType,
|
||||
status: 'CANCELLED', replayed: true, remainingPalletSpaces: 12 }
|
||||
}
|
||||
|
||||
export async function verifyPoolBindingRevalidation({ offerDetail, replan, resume, blockedConfirm, check }) {
|
||||
const invalidated = (await offerDetail()).offer
|
||||
check('Binding invalidates capacity while publication remains OPEN', invalidated.status === 'OPEN'
|
||||
&& !invalidated.capacityKnown && !invalidated.canConfirm && /计划|核对/.test(id(invalidated.blockReason)))
|
||||
await blockedConfirm('BIND-INVALIDATED')
|
||||
await replan()
|
||||
const recalculated = (await offerDetail()).offer
|
||||
check('Recalculating cargo alone cannot silently resume acceptance', recalculated.status === 'OPEN'
|
||||
&& !recalculated.capacityKnown && !recalculated.canConfirm && /恢复接单|承诺已变化/.test(id(recalculated.blockReason)))
|
||||
await blockedConfirm('REPLANNED-NOT-RESUMED')
|
||||
const restored = await resume(recalculated)
|
||||
check('Only explicit resume makes current capacity confirmable', restored.status === 'OPEN' && restored.capacityKnown && restored.canConfirm)
|
||||
return restored
|
||||
}
|
||||
|
||||
export function poolPickScope(context, member) {
|
||||
requireThat(context?.marker && /^[1-9]\d*$/.test(id(member?.customerId)) && member?.dispatchNo,
|
||||
'EXACT_CUSTOMER_DISPATCH_PICK_SCOPE_REQUIRED')
|
||||
@@ -261,6 +338,20 @@ async function poolScenario(context, fixture, { execute = false, readInventory,
|
||||
const latest = no => request('GET', `${POOL}/inquiries/${no}`, undefined, dispatchers[0])
|
||||
const command = (inquiry, label) => ({ tripRecordVersion: inquiry.tripRecordVersion, offerRecordVersion: inquiry.offerRecordVersion,
|
||||
recordVersion: inquiry.recordVersion, idempotencyKey: key(label) })
|
||||
const ownershipBefore = await latest(first.bookingNo), sensitiveTripBefore = await request('GET', tripPath)
|
||||
const ownerOfferBefore = (await offerDetail(dispatchers[0])).offer
|
||||
await reject('Other sales cannot edit an owned inquiry', 'PUT', `${POOL}/inquiries/${first.bookingNo}`,
|
||||
poolInquiryWrite(ownershipBefore, key('OTHER-SALES-EDIT')), sales[1])
|
||||
await reject('Other sales cannot cancel an owned inquiry', 'POST', `${POOL}/inquiries/${first.bookingNo}/cancel`,
|
||||
{ ...command(ownershipBefore, 'OTHER-SALES-CANCEL'), reason: 'TEST越权负例' }, sales[1])
|
||||
const sensitiveRead = await raw('GET', tripPath, undefined, sales[1])
|
||||
check('Minimal sales cannot read sensitive operational trip', poolReadIsDenied(sensitiveRead)
|
||||
&& sensitiveRead.requestId && sensitiveRead.traceId, sensitiveRead)
|
||||
check('Rejected sales writes and reads preserve original owner draft and trip facts',
|
||||
inquiryFact(ownershipBefore) === inquiryFact(await latest(first.bookingNo))
|
||||
&& JSON.stringify(sensitiveTripBefore) === JSON.stringify(await request('GET', tripPath))
|
||||
&& capacitySnapshot(ownerOfferBefore) === capacitySnapshot((await offerDetail(dispatchers[0])).offer))
|
||||
proof.covered.push('other-sales-write-and-sensitive-trip-denied')
|
||||
await verifyAssignedVehicleGuards(context, fixture, tripProof, { request, reject, check, offerDetail: () => offerDetail(dispatchers[0]), proof })
|
||||
await verifyPauseAndCutoff({ offerNo: proof.offerNo, bookingNo: first.bookingNo, salesToken: sales[0],
|
||||
dispatcherToken: dispatchers[0], request, reject, check, key, proof })
|
||||
@@ -280,12 +371,20 @@ async function poolScenario(context, fixture, { execute = false, readInventory,
|
||||
proof.dimensionRejections.push({ dimension, bookingNo: inquiry.bookingNo, submitted: inquiry.cargo[dimension], available: available[dimension], status: 'REJECTED_UNCHANGED' })
|
||||
}
|
||||
proof.covered.push('weight-and-volume-independent-capacity-rejections')
|
||||
proof.cancelledOrders = []
|
||||
for (const [index, type] of ['LOCAL_LOOSEBOARD', 'US_TRUCK'].entries()) {
|
||||
const inquiry = await createInquiry(index, type, 1, `CANCEL-${type}`)
|
||||
proof.cancelledOrders.push(await verifyPoolConfirmedCancellation({ inquiry, destinationKey, request, check, latest,
|
||||
command, businessPath, dispatcherToken: dispatchers[index], label: `CANCEL-${type}` }))
|
||||
}
|
||||
proof.covered.push('confirmed-cancel-before-incoming-releases-capacity-and-replays')
|
||||
let current = await latest(first.bookingNo), firstCommand = command(current, 'CONFIRM-LOOSE')
|
||||
await reject('Sales cannot confirm capacity', 'POST', `${POOL}/inquiries/${first.bookingNo}/confirm`, firstCommand, sales[0])
|
||||
const firstConfirmed = await request('POST', `${POOL}/inquiries/${first.bookingNo}/confirm`, firstCommand, dispatchers[0])
|
||||
assertPoolRemaining(firstConfirmed.offer, destinationKey, 4)
|
||||
proof.covered.push('same-segment-plan-18-of-30-reserve-8-leaves-4')
|
||||
const oversized = await createInquiry(1, 'US_TRUCK', 5, 'OVER-CAPACITY')
|
||||
proof.revalidationDraftNo = oversized.bookingNo
|
||||
await reject('Five spaces cannot fit last four', 'POST', `${POOL}/inquiries/${oversized.bookingNo}/confirm`, command(await latest(oversized.bookingNo), 'REJECT-FIVE'), dispatchers[1])
|
||||
const rival = await createInquiry(0, 'US_TRUCK', 4, 'TRUCK-B')
|
||||
const contenders = [await latest(second.bookingNo), await latest(rival.bookingNo)]
|
||||
@@ -295,16 +394,6 @@ async function poolScenario(context, fixture, { execute = false, readInventory,
|
||||
offer = (await offerDetail(dispatchers[0])).offer; assertPoolRemaining(offer, destinationKey, 0)
|
||||
proof.orders = [firstConfirmed.inquiry, winner]
|
||||
proof.covered.push('two-dispatchers-last-four-race')
|
||||
const closed = await request('POST', `${POOL}/${proof.offerNo}/close`, { tripRecordVersion: offer.tripRecordVersion,
|
||||
offerRecordVersion: offer.recordVersion, idempotencyKey: key('CLOSE'), reason: context.marker }, dispatchers[0])
|
||||
check('Offer closed without cancelling commitments', closed.offer.status === 'CLOSED')
|
||||
const replay = await request('POST', `${POOL}/inquiries/${first.bookingNo}/confirm`, firstCommand, dispatchers[0])
|
||||
assertSingleCommercial(firstConfirmed.inquiry, replay.inquiry)
|
||||
check('Successful confirmation still replays after close', replay.replayed === true)
|
||||
const history = await request('GET', `${POOL}/inquiries?page=1&size=100&scope=MINE`, undefined, sales[0])
|
||||
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')
|
||||
const poolMembers = []
|
||||
for (const [index, order] of proof.orders.entries()) {
|
||||
const source = await request('GET', businessPath(order))
|
||||
@@ -313,9 +402,32 @@ async function poolScenario(context, fixture, { execute = false, readInventory,
|
||||
const oldStatusPath = businessPath(order).replace('?', '/status?')
|
||||
await reject('Old commercial status mutation cannot bypass pool workflow', 'PATCH', oldStatusPath,
|
||||
{ version: source.summary.version, status: 'CANCELLED', idempotencyKey: key(`OLD-CANCEL-${index}`), remark: context.marker })
|
||||
poolMembers.push(await receiveAndBindPoolOrder(context, fixture, order, index, { request, check, key, readInventory }))
|
||||
const otherCustomer = merchantSessions.find(row => row.customerId !== order.customerId)
|
||||
check('Cross-customer negative has a genuinely different qualified customer', Boolean(otherCustomer))
|
||||
const merchantPath = `/api/merchant/v1/last-leg/${order.businessType === 'US_TRUCK' ? 'us-truck-orders' : 'local-looseboard-orders'}/${encode(order.businessOrderNo)}`
|
||||
const foreignRead = await raw('GET', merchantPath, undefined, otherCustomer.accessToken)
|
||||
check('Another customer cannot read this commercial order', poolReadIsDenied(foreignRead)
|
||||
&& foreignRead.requestId && foreignRead.traceId, foreignRead)
|
||||
check('Denied old status and foreign read preserve commercial source', JSON.stringify(source) === JSON.stringify(await request('GET', businessPath(order))))
|
||||
poolMembers.push(await receiveAndBindPoolOrder(context, fixture, order, index, { request, reject, check, key, readInventory,
|
||||
latest, command, businessPath, offerDetail: () => offerDetail(dispatchers[0]), dispatcherToken: dispatchers[0], destinationKey, proof }))
|
||||
}
|
||||
proof.covered.push('old-manual-order-endpoints-rejected', 'one-commercial-order-and-one-incoming', 'both-business-types-real-pda-receipt')
|
||||
proof.covered.push('old-manual-order-endpoints-rejected', 'one-commercial-order-and-one-incoming', 'both-business-types-real-pda-receipt',
|
||||
'other-customer-commercial-read-denied-with-source-unchanged', 'incoming-cancel-rejected-with-source-unchanged',
|
||||
'receipt-overcapacity-rejected-with-source-unchanged', 'open-offer-bind-replan-requires-explicit-resume', 'bound-cargo-reservation-removed-without-double-count')
|
||||
offer = (await offerDetail(dispatchers[0])).offer
|
||||
const beforeClose = await Promise.all(proof.orders.map(row => latest(row.bookingNo)))
|
||||
const closed = await request('POST', `${POOL}/${proof.offerNo}/close`, { tripRecordVersion: offer.tripRecordVersion,
|
||||
offerRecordVersion: offer.recordVersion, idempotencyKey: key('CLOSE'), reason: context.marker }, dispatchers[0])
|
||||
check('Offer closed without cancelling commitments', closed.offer.status === 'CLOSED')
|
||||
const replay = await request('POST', `${POOL}/inquiries/${first.bookingNo}/confirm`, firstCommand, dispatchers[0])
|
||||
assertSingleCommercial(firstConfirmed.inquiry, replay.inquiry)
|
||||
check('Successful confirmation still replays after close', replay.replayed === true)
|
||||
for (const row of beforeClose) check('Old confirmation replay cannot revert bound source', inquiryFact(row) === inquiryFact(await latest(row.bookingNo)))
|
||||
const history = await request('GET', `${POOL}/inquiries?page=1&size=100&scope=MINE`, undefined, sales[0])
|
||||
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' })
|
||||
const members = [...fixture.members, ...poolMembers]
|
||||
@@ -371,7 +483,8 @@ async function poolScenario(context, fixture, { execute = false, readInventory,
|
||||
}
|
||||
}
|
||||
|
||||
async function receiveAndBindPoolOrder(context, fixture, order, index, { request, check, key, readInventory }) {
|
||||
async function receiveAndBindPoolOrder(context, fixture, order, index, { request, reject, check, key, readInventory,
|
||||
latest, command, businessPath, offerDetail, dispatcherToken, destinationKey, proof }) {
|
||||
const tripPath = `${API}/fleet/trips/${encode(fixture.tripNo)}`, bookingPath = `${tripPath}/bookings/${encode(order.bookingNo)}`
|
||||
let bookingList = await request('GET', `${tripPath}/bookings`), booking = bookingList.records.find(row => row.bookingNo === order.bookingNo)
|
||||
check('Pool commercial association survived into existing booking workflow', booking?.status === 'CONFIRMED')
|
||||
@@ -380,6 +493,15 @@ async function receiveAndBindPoolOrder(context, fixture, order, index, { request
|
||||
const created = await request('POST', `${bookingPath}/incoming`, body), incomingNo = created.booking.incomingNo
|
||||
const repeated = await request('POST', `${bookingPath}/incoming`, body)
|
||||
check('One incoming document on exact replay', incomingNo && repeated.replayed === true && repeated.booking.incomingNo === incomingNo)
|
||||
const incomingBeforeCancel = await request('GET', `${bookingPath}/incoming`), sourceBeforeCancel = await request('GET', businessPath(order))
|
||||
const inquiryBeforeCancel = await latest(order.bookingNo), capacityBeforeCancel = (await offerDetail()).offer
|
||||
await reject('Incoming already registered cannot be cancelled from pool', 'POST', `${POOL}/inquiries/${order.bookingNo}/cancel`,
|
||||
{ ...command(inquiryBeforeCancel, `CANCEL-AFTER-INCOMING-${index}`), reason: 'TEST已来货取消拒绝' }, dispatcherToken)
|
||||
check('Rejected incoming cancellation preserves incoming commercial booking and capacity',
|
||||
JSON.stringify(incomingBeforeCancel) === JSON.stringify(await request('GET', `${bookingPath}/incoming`))
|
||||
&& JSON.stringify(sourceBeforeCancel) === JSON.stringify(await request('GET', businessPath(order)))
|
||||
&& inquiryFact(inquiryBeforeCancel) === inquiryFact(await latest(order.bookingNo))
|
||||
&& capacitySnapshot(capacityBeforeCancel) === capacitySnapshot((await offerDetail()).offer))
|
||||
const pdaPath = `/api/pda/v1/last-leg/warehouse-transfers/${encode(incomingNo)}`
|
||||
const measured = await request('POST', `${pdaPath}/pallets/drafts`, { idempotencyKey: key(`MEASURE-${index}`), cartonCount: 1,
|
||||
lengthCm: 100, widthCm: 100, heightCm: 100, volumeCbm: 1, grossWeightKg: 100 }, fixture.pdaToken)
|
||||
@@ -393,6 +515,21 @@ async function receiveAndBindPoolOrder(context, fixture, order, index, { request
|
||||
const received = await request('POST', `${pdaPath}/palletize`, { idempotencyKey: key(`PALLETIZE-${index}`), photoFileIds: [id(uploaded.body.data.fileId)] }, fixture.pdaToken)
|
||||
check('Actual PDA receipt completed one original pallet', received.status === 'WAIT_CUSTOMER_CONFIRMATION' && received.pallets.length === 1 && received.pallets[0].status === 'COMPLETED')
|
||||
const evidence = await request('GET', `${bookingPath}/incoming`)
|
||||
const beforeOverReceipt = await latest(order.bookingNo), sourceBeforeReceipt = await request('GET', businessPath(order))
|
||||
const capacityBeforeReceipt = (await offerDetail()).offer, tripBeforeReceipt = await request('GET', tripPath)
|
||||
const inventoryBeforeReceipt = await readInventory({ marker: context.marker, customerId: order.customerId, warehouseId: fixture.warehouseId })
|
||||
const overReceipt = await reject('Actual received pallet spaces cannot exceed whole vehicle capacity', 'POST', `${bookingPath}/receipt-confirmation`,
|
||||
{ tripRecordVersion: evidence.tripRecordVersion, recordVersion: evidence.recordVersion, receiptHash: evidence.incoming.receiptHash,
|
||||
pallets: [{ warehousePalletId: id(pallet.warehousePalletId), palletSpaces: Number(tripBeforeReceipt.trip.maxPalletSpaces) + 1 }],
|
||||
unitPrice: 30, idempotencyKey: key(`OVER-RECEIPT-${index}`) })
|
||||
check('Receipt overrun is refused by actual capacity calculation', /超过.*限制|容量|载重|容积/.test(id(overReceipt.body?.message)))
|
||||
check('Rejected overreceipt leaves source receipt commercial booking and reservations unchanged',
|
||||
JSON.stringify(evidence) === JSON.stringify(await request('GET', `${bookingPath}/incoming`))
|
||||
&& inquiryFact(beforeOverReceipt) === inquiryFact(await latest(order.bookingNo))
|
||||
&& JSON.stringify(sourceBeforeReceipt) === JSON.stringify(await request('GET', businessPath(order)))
|
||||
&& capacitySnapshot(capacityBeforeReceipt) === capacitySnapshot((await offerDetail()).offer))
|
||||
check('Rejected overreceipt leaves complete customer warehouse inventory unchanged', JSON.stringify(inventoryBeforeReceipt)
|
||||
=== JSON.stringify(await readInventory({ marker: context.marker, customerId: order.customerId, warehouseId: fixture.warehouseId })))
|
||||
await request('POST', `${bookingPath}/receipt-confirmation`, { tripRecordVersion: evidence.tripRecordVersion, recordVersion: evidence.recordVersion,
|
||||
receiptHash: evidence.incoming.receiptHash, pallets: [{ warehousePalletId: id(pallet.warehousePalletId), palletSpaces: 1 }], unitPrice: 30, idempotencyKey: key(`RECEIPT-${index}`) })
|
||||
const inventory = await readInventory({ marker: context.marker, customerId: order.customerId, warehouseId: fixture.warehouseId })
|
||||
@@ -408,9 +545,32 @@ async function receiveAndBindPoolOrder(context, fixture, order, index, { request
|
||||
await request('POST', `${bookingPath}/dispatch`, { tripRecordVersion: bookingList.tripRecordVersion, recordVersion: booking.recordVersion,
|
||||
dispatchNo: dispatch.dispatchNo, idempotencyKey: key(`BIND-DISPATCH-${index}`) })
|
||||
let plan = await request('GET', `${tripPath}/capacity-plan`), detail = await request('GET', tripPath)
|
||||
// Reconfirm actual current cargo after binding; remaining unbound bookings stay separately reserved.
|
||||
await request('POST', `${tripPath}/capacity-plan`, { recordVersion: plan.recordVersion, manifestHash: plan.manifestHash,
|
||||
pallets: controlledPalletMeasurements(plan.cargo, detail.trip.stops), reason: `${context.marker} 实收后重新核对,不重复扣外接预留` })
|
||||
const actualMeasurements = controlledPalletMeasurements(plan.cargo, detail.trip.stops)
|
||||
const restored = await verifyPoolBindingRevalidation({ offerDetail, check,
|
||||
blockedConfirm: async label => {
|
||||
const before = await latest(proof.revalidationDraftNo)
|
||||
const rejected = await reject('Stale OPEN publication cannot confirm another draft', 'POST', `${POOL}/inquiries/${before.bookingNo}/confirm`,
|
||||
command(before, `${label}-${index}`), dispatcherToken)
|
||||
check('Confirmation refused specifically for stale plan or publication commitment', /计划|核对|恢复接单|承诺已变化/.test(id(rejected.body?.message)))
|
||||
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} 实收后重新核对,不重复扣外接预留` }),
|
||||
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)
|
||||
check('Bound booking points to this actual incoming and dispatch', bound.status === 'BOUND' && bound.incomingNo === incomingNo && bound.dispatchNo === dispatch.dispatchNo)
|
||||
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 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,
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user