This commit is contained in:
2026-10-20 15:48:38 +08:00
parent da6b4685d7
commit 0ff1eefff6
50 changed files with 3933 additions and 78 deletions
@@ -32,11 +32,19 @@ export const preparationContract = Object.freeze({
})
/** Preparation is opt-in and only for separately authorized TEST execution. Import/default call is offline. */
export async function createFleetCrossCustomerFixture(context, { execute = false, warehouseId, destinationWarehouseId, pdaToken, readInventory } = {}) {
export async function createFleetCrossCustomerFixture(context, { execute = false, warehouseId, destinationWarehouseId, destinationWarehouseIds,
shipmentReferences, prepareBatchAllocations, pdaToken, readInventory } = {}) {
if (!execute) return preparationContract
if (!context?.marker || !id(warehouseId) || !id(destinationWarehouseId) || !pdaToken || typeof readInventory !== 'function'
|| ['request', 'requestWithBody', 'requestMultipart', 'findAvailablePhone', 'apiSuccess', 'assert'].some(name => typeof context[name] !== 'function'))
throw new Error('Missing current-run context, public warehouse/destination, caller-owned PDA identity or raw inventory reader')
const memberDestinations = destinationWarehouseIds ?? [destinationWarehouseId, destinationWarehouseId]
if (shipmentReferences !== undefined && (!Array.isArray(shipmentReferences) || shipmentReferences.length !== 2
|| shipmentReferences.some(value => typeof value !== 'string' || !value.trim()))) throw Error('Two explicit TEST shipment references required')
if (prepareBatchAllocations !== undefined && typeof prepareBatchAllocations !== 'function') throw Error('Appointment preparation must be callable')
if (!Array.isArray(memberDestinations) || memberDestinations.length !== 2
|| memberDestinations.some(value => !id(value)) || id(memberDestinations[0]) !== id(destinationWarehouseId))
throw new Error('Two explicit member destinations must start with the primary destination')
context.createdResources ??= {}
if (context.createdResources.fleetCrossCustomerFixture) throw new Error('Fixture preparation already started; retain partial resources and reconcile explicitly')
const proof = context.createdResources.fleetCrossCustomerFixture = {
@@ -106,18 +114,23 @@ export async function createFleetCrossCustomerFixture(context, { execute = false
try {
// Explicit IDs are re-read through the public master-data API before any fixture writes.
const origin = await request('GET', `/api/platform/v1/warehouse-admin/${encode(warehouseId)}?tenantId=0`, undefined, 'Verify receiving warehouse')
const destination = await request('GET', `/api/platform/v1/warehouse-admin/${encode(destinationWarehouseId)}?tenantId=0`, undefined, 'Verify FBA destination')
const validMaster = (detail, expectedId, type) => id(detail?.warehouse?.id) === id(expectedId)
&& id(detail.warehouse.tenantId) === '0' && detail.warehouse.status === 'ACTIVE' && detail.warehouse.warehouseType === type
&& detail.warehouse.warehouseCode && detail.warehouse.warehouseName
check('Origin and destination are current public active warehouse masters', validMaster(origin, warehouseId, 'OVERSEAS')
&& origin.capabilities?.some(row => row.capabilityCode === 'RECEIVING' && row.enabled === true)
&& validMaster(destination, destinationWarehouseId, 'FBA'))
const address = destination.addresses?.find(row => row.addressLine1)
check('FBA destination has a real address', Boolean(address))
const deliveryAddress = [address.countryCode, address.stateProvince, address.city, address.district,
address.addressLine1, address.addressLine2, address.postalCode].filter(Boolean).join(' ')
&& origin.capabilities?.some(row => row.capabilityCode === 'RECEIVING' && row.enabled === true))
const destinations = new Map()
for (const destinationId of [...new Set(memberDestinations.map(id))]) {
const destination = await request('GET', `/api/platform/v1/warehouse-admin/${encode(destinationId)}?tenantId=0`, undefined, 'Verify FBA destination')
check('Each explicit destination is a public active FBA master', validMaster(destination, destinationId, 'FBA'))
const address = destination.addresses?.find(row => row.addressLine1)
check('FBA destination has a real address', Boolean(address))
destinations.set(destinationId, { destination, deliveryAddress: [address.countryCode, address.stateProvince, address.city, address.district,
address.addressLine1, address.addressLine2, address.postalCode].filter(Boolean).join(' ') })
}
for (const suffix of ['A', 'B']) {
const memberDestinationId = id(memberDestinations[suffix === 'A' ? 0 : 1])
const { destination, deliveryAddress } = destinations.get(memberDestinationId)
const member = { marker: context.marker, customerMarker: `${context.marker}-CROSS-${suffix}`, receiptVerified: false }
proof.members.push(member)
// Qualification helper's mutable fixture, options and resources must not touch the parent's merchant slot.
@@ -147,11 +160,12 @@ export async function createFleetCrossCustomerFixture(context, { execute = false
const profile = await request('GET', '/api/merchant/v1/iam/tenant/profile', undefined, 'Verify newly authenticated customer identity', auth.accessToken)
check('Merchant session belongs to the newly qualified customer', id(profile?.id) === member.customerId)
const createBody = { sourceLastlegOrderIds: [], operatingWarehouseId: id(warehouseId),
fbaWarehouseCode: destination.warehouse.warehouseCode, destinationType: 'FBA', destinationWarehouseId: id(destinationWarehouseId),
fbaWarehouseCode: destination.warehouse.warehouseCode, destinationType: 'FBA', destinationWarehouseId: memberDestinationId,
destinationWarehouseCode: destination.warehouse.warehouseCode, destinationWarehouseName: destination.warehouse.warehouseName,
deliveryAddress, packageCount: 1, volumeCbm: 1, deliveryMethod: 'CUSTOMER_DELIVERY', estimatedArrivalAt: localTime(1800000),
needRelabel: false, remark: member.customerMarker, idempotencyKey: key(`CREATE-${suffix}`),
items: [{ skuCode: `CROSS-${key(suffix).slice(0, 16)}`, productNameZh: '受控测试一箱货物', packageCount: 1, quantity: 1 }] }
if (shipmentReferences) createBody.fbaShipmentNo = shipmentReferences[proof.members.length - 1]
let incoming = await request('POST', MERCHANT, createBody, `Create new incoming ${suffix}`, auth.accessToken)
member.incomingNo = incoming?.transferNo
check('Ordinary transfer creates fresh incoming without borrowed source inventory', member.incomingNo && incoming.inboundCode === member.incomingNo
@@ -234,6 +248,20 @@ export async function createFleetCrossCustomerFixture(context, { execute = false
proof.createDispatchRequest = { idempotencyKey: key('CREATE_ONE_NATIVE_DISPATCH'), warehouseId: id(warehouseId),
vehicle: null, driver: null, plannedDepartureAt: null, estimatedArrivalAt: null,
remark: `${context.marker} TEST跨客户同仓一张排车一趟车`, cargo: proof.members.map(member => member.selection), batchAllocations: [] }
if (prepareBatchAllocations) {
proof.createDispatchRequest.batchAllocations = await prepareBatchAllocations(proof.members)
if (!Array.isArray(proof.createDispatchRequest.batchAllocations)) throw Error('Formal appointment allocations must be explicit')
for (const member of proof.members) {
const current = await request('GET', `${ROOT}/dispatches/candidates?${new URLSearchParams({ warehouseId: id(warehouseId), orderNo: member.incomingNo })}`,
undefined, 'Refresh actual candidate after formal appointment creation')
const matches = current.candidates?.filter(row => id(row.customerId) === member.customerId && id(row.selection?.warehousePalletId) === member.palletId)
check('Appointment preparation preserves exact received physical source', matches?.length === 1 && current.complete === true
&& matches[0].selection?.sourceType === 'INBOUND_PLAN' && matches[0].blockReasons?.length === 0)
member.selection = matches[0].selection
}
proof.createDispatchRequest.cargo = proof.members.map(member => member.selection)
await context.checkpoint?.()
}
const created = await request('POST', `${ROOT}/dispatches`, proof.createDispatchRequest, 'Create one native dispatch from both real customers')
check('One native Create returns both unpicked pallets without client-supplied customer identity', created?.dispatchNo && created.status === 'ACTIVE'
&& created.stage === 'BEFORE_LOADING' && safeFixtureCount(created.counts?.packageCount) === 2 && safeFixtureCount(created.counts?.pickedPackageCount) === 0 && safeFixtureCount(created.counts?.shippedPackageCount) === 0)