test(acceptance): reuse fixed customers and align native fulfillment flows

This commit is contained in:
2026-10-23 04:48:02 +08:00
parent b92f7fe991
commit 139f6b1932
51 changed files with 1076 additions and 228 deletions
@@ -1,3 +1,5 @@
import { NODE_SEQUENCES, STANDARD_NODES, manualNodePayload } from './phase4-carrier-nodes.mjs'
const MODULE = 'phase4-overseas-inbound-operations'
const OPTIONS = { scenario: 'phase4-inbound-admin-and-inventory-commands', deploymentBlocker: true }
const PLAN_BASE = '/api/platform/v1/last-leg/inbound-plans'
@@ -112,12 +114,14 @@ export async function prepareAppointmentFixture(context, { customerId, plan, fba
&& history.records[0].snapshot?.plannedDate === detail.plannedDate, appointmentNo)
for (const v of verified) {
const restored = await request(context, 'GET', '取消后逐票可分配量恢复', query(`${APPOINTMENT_BASE}/sources/${encodeURIComponent(v.sourceKey)}/cargo-verification`, { customerId, destinationKey: v.destinationKey }))
requireProof(context, '取消未删除核实事实且恢复剩余需求', count(restored.allocatedPackages) === 0 && count(restored.availablePackages) === v.packageCount
requireProof(context, '取消保留核实事实及原批次货物分配', count(restored.allocatedPackages) === v.packageCount && count(restored.availablePackages) === 0
&& restored.verification?.version === v.cargoVerificationVersion && restored.sourceChanged === false, v.sourceKey)
}
const active = await request(context, 'POST', '为本轮后续验收重新建立有效批次', `${PLAN_BASE}/fba-appointments/batch`, { ...createBody, idempotencyKey: `${createBody.idempotencyKey}-active` })
requireProof(context, '重新安排生成新批次且逐票数量不变', active.batchKey === `FBA_APPOINTMENT:${active.id}` && active.batchKey !== cancelled.batchKey
&& active.appointmentNo !== appointmentNo && active.appointmentStatus === 'PENDING' && JSON.stringify(exactItems(active)) === JSON.stringify(exactItems(detail)), active?.appointmentNo || 'missing')
const active = await request(context, 'PUT', '在原批次重新安排本轮有效预约', detailPath,
{ version: cancelled.version, idempotencyKey: `${createBody.idempotencyKey}-active`, details: createBody.details })
requireProof(context, '重新安排复用原批次且逐票数量不变', active.batchKey === cancelled.batchKey
&& active.appointmentNo === appointmentNo && active.version === cancelled.version + 1
&& active.appointmentStatus === 'PENDING' && JSON.stringify(exactItems(active)) === JSON.stringify(exactItems(detail)), active?.appointmentNo || 'missing')
const activeContext = await request(context, 'GET', '回查保留的有效批次', `${APPOINTMENT_BASE}/batches/${encodeURIComponent(active.batchKey)}`)
requireProof(context, '保留批次上下文完整可供后续HTTP核对', activeContext?.appointment?.batchKey === active.batchKey
&& activeContext.appointment.appointmentStatus === 'PENDING', active.batchKey)
@@ -242,6 +246,7 @@ async function inboundAdminLifecycle(context) {
&& id(supplement?.destinationWarehouseId) === id(sample.destinationWarehouseId),
`palletId=${id(supplement?.id) || 'missing'}, status=${supplement?.assignmentStatus || 'missing'}`, OPTIONS)
await ensureAppointmentDeparture(context, producer, order.id)
const appointmentProof = await prepareAppointmentFixture(context, { customerId, plan, fbaLines })
context.createdResources.phase4OverseasInboundAdmin = {
marker: context.marker, customerId, orderId: id(order.id), planNo,
@@ -249,6 +254,54 @@ async function inboundAdminLifecycle(context) {
}
}
/** 使用本轮自动船期任务推进实际开船事实,再等待尾程消费,不能用人工订单节点绕过来源校验。 */
export async function ensureAppointmentDeparture(context, producer, orderId) {
if (producer?.marker !== context.marker || !id(producer.carrierScheduleQueryId)
|| !id(producer.containerBookingId) || !id(producer.tenantId) || !id(orderId))
throw Error('预约开船前置缺少本轮船期任务身份')
const base = `/api/platform/v1/carrier-schedule-queries/${encodeURIComponent(id(producer.carrierScheduleQueryId))}`
let detail = await request(context, 'GET', '核对预约来源的原生船期任务', base)
const task = detail?.query?.query
if (id(task?.id) !== id(producer.carrierScheduleQueryId) || id(task?.customerId) !== id(producer.tenantId)
|| id(task?.containerBookingId) !== id(producer.containerBookingId) || task?.trackingNo !== producer.containerNo)
throw Error('预约来源船期任务客户或柜身份不一致')
const proof = context.createdResources.appointmentDeparturePrerequisite = {
marker: context.marker, queryId: id(task.id), containerBookingId: id(producer.containerBookingId), commands: [], status: 'PREPARING',
}
for (const node of STANDARD_NODES.slice(0, 4)) {
const current = detail.query.query
if ((NODE_SEQUENCES[current.latestActualNode] || 0) >= NODE_SEQUENCES[node]) continue
const now = new Date()
const local = value => new Date(value.getTime() - value.getTimezoneOffset() * 60000).toISOString().slice(0, 19)
const priorAt = current.latestActualAt && Date.parse(current.latestActualAt)
const actualAt = Number.isFinite(priorAt) ? new Date(priorAt + 60000) : new Date(now.getTime() - 10 * 60000)
const body = manualNodePayload({ expectedVersion: Number(current.version), standardNode: node,
observedAt: local(now), actualAt: local(actualAt), marker: context.marker })
if (!Number.isSafeInteger(body.expectedVersion) || body.expectedVersion < 0) throw Error('预约来源船期版本无效')
const headers = { 'Idempotency-Key': key(context, `appointment-departure-${node}`) }
const command = { body, idempotencyKey: headers['Idempotency-Key'], status: 'PREPARED' }
proof.commands.push(command); await context.checkpoint?.()
command.status = 'UNKNOWN_RESULT'
await request(context, 'POST', '补齐预约来源原生实际船节点', `${base}/manual-nodes`, body, headers)
command.status = 'SUCCEEDED'
detail = await request(context, 'GET', '回查原生实际船节点', base)
if ((NODE_SEQUENCES[detail?.query?.query?.latestActualNode] || 0) < NODE_SEQUENCES[node])
throw Error('实际船节点补录未推进原船期任务')
}
for (let attempt = 1; attempt <= 30; attempt++) {
const response = await context.request('GET', query(`/api/platform/v1/last-leg/orders/${encodeURIComponent(id(orderId))}`,
{ customerId: id(producer.tenantId) }))
const order = response?.body?.data?.order
if (ok(response) && id(order?.customerId) === id(producer.tenantId)
&& id(order?.sourceContainerBookingId) === id(producer.containerBookingId)
&& !order.manualNodeOverride && (NODE_SEQUENCES[order.containerNode] || 0) >= 40) {
proof.status = 'CONSUMED'; return proof
}
if (attempt < 30) await new Promise(resolve => setTimeout(resolve, 1000))
}
throw Error('预约来源开船事实未被尾程消费,停止预约写入')
}
async function inventoryCommandLifecycle(context) {
const pda = context.createdResources?.phase4OverseasPda
const customerId = id(pda?.customerId)