test(fba): preserve verified fleet and fulfillment acceptance workflows
This commit is contained in:
@@ -6,6 +6,8 @@ import { awaitProducerConsumption, convertReceivingContainers, runReceiving, res
|
||||
resolvePickerAccess, createPicker, disablePicker } from './phase4-overseas-pda.mjs'
|
||||
import { pickAll, verifyDepartureReadiness, saveReceiptAndComplete } from './phase5-overseas-fulfillment.mjs'
|
||||
import { prepareTerminalDrayage } from './phase5-terminal-drayage.mjs'
|
||||
import { prepareTaxCustoms, verifyTaxDocumentsAfterCompletion } from './phase5-root-tax-customs.mjs'
|
||||
import { prepareFleetTrip, confirmFleetLoading, finishFleetTrip } from './fleet-trip-fulfillment.mjs'
|
||||
|
||||
const MODULE = 'phase5-root-fulfillment'
|
||||
const OPTIONS = { scenario: 'f40-real-root-and-remainder-signatures', deploymentBlocker: true }
|
||||
@@ -18,6 +20,7 @@ const query = (path, values) => `${path}?${new URLSearchParams(values)}`
|
||||
export function f40LocalDateTime(value) { return value.toISOString().slice(0,19) }
|
||||
export function preserveReceivingEvidence(proof, scope) {
|
||||
proof.receivingCompletionEvidence = scope.createdResources?.phase4OverseasPda?.receivingCompletionEvidence || {}
|
||||
proof.receivingCandidateEvidence = scope.createdResources?.phase4OverseasPda?.candidateWaitEvidence || []
|
||||
}
|
||||
export function validFreshReceiptSave(before, after) {
|
||||
const previousVersion = Number(before?.documents?.version)
|
||||
@@ -234,6 +237,36 @@ export async function verifyRootProjections(context, customerId, rootOrderId, me
|
||||
&& customer.summary.mainStatus === platform.summary.mainStatus,'exact-root-platform-customer-status')
|
||||
return {platform:platform.summary,customer:customer.summary}
|
||||
}
|
||||
export async function verifyRemainderTaxIdentity(context, evidence, merchantToken) {
|
||||
const {taxMode,customerId,rootOrderId,childOrderId,rootOrderNo,rawChild,rootInbound,childInbound}=evidence
|
||||
check(context,'F40 余量税制核对范围完整',['BUY_DECLARATION','TAX_REFUND'].includes(taxMode)
|
||||
&& Boolean(rootOrderNo && customerId && rootOrderId && childOrderId),'exact-current-remainder-scope')
|
||||
check(context,'F40 原始余量子订单继承本轮税制',rawChild?.taxMode===taxMode
|
||||
&& id(rawChild.id)===childOrderId && id(rawChild.tenantId)===customerId,
|
||||
`expected=${taxMode}, actual=${rawChild?.taxMode || 'missing'}`)
|
||||
check(context,'F40 根与余量入仓摘要税制和来源身份一致',rootInbound?.taxMode===taxMode
|
||||
&& childInbound?.taxMode===taxMode && id(rootInbound.orderId)===rootOrderId
|
||||
&& id(childInbound.orderId)===childOrderId && id(childInbound.tenantId)===customerId,
|
||||
`rootTax=${rootInbound?.taxMode || 'missing'}, childTax=${childInbound?.taxMode || 'missing'}`)
|
||||
const exactRows = page => Array.isArray(page?.records) && page.records.length===2 && Number(page.total)===2
|
||||
&& new Set(page.records.map(row=>id(row.orderId))).size===2
|
||||
&& page.records.every(row=>[rootOrderId,childOrderId].includes(id(row.orderId))
|
||||
&& id(row.tenantId)===customerId && row.taxMode===taxMode
|
||||
&& id(row.relation?.rootOrderId)===rootOrderId
|
||||
&& (id(row.orderId)===rootOrderId?row.relation?.role==='PARENT'
|
||||
:row.relation?.role==='CHILD'&&id(row.relation?.parentOrderId)===rootOrderId))
|
||||
for(const [name,filter] of [['unfiltered',null],['matchingTax',taxMode],
|
||||
['otherTax',taxMode==='TAX_REFUND'?'BUY_DECLARATION':'TAX_REFUND']]) {
|
||||
const values={orderNo:rootOrderNo,page:'1',size:'200'}
|
||||
if(filter)values.taxMode=filter
|
||||
evidence[name]=await api(context,'GET',`F40 客户统一订单税制筛选对照${name}`,
|
||||
query('/api/merchant/v1/orders',values),undefined,merchantToken)
|
||||
check(context,`F40 客户筛选${name}精确覆盖本轮父子身份`,name==='otherTax'
|
||||
? Array.isArray(evidence[name]?.records)&&evidence[name].records.length===0&&Number(evidence[name].total)===0
|
||||
: exactRows(evidence[name]),`taxMode=${filter || 'unfiltered'}`)
|
||||
}
|
||||
return evidence
|
||||
}
|
||||
export async function cancelUnloadedRemainder(context, proof, customerId, rootOrderId, childOrderId,
|
||||
rootInboundId, childInboundId) {
|
||||
proof.currentStage = 'REMAINDER_CANCEL_PRECONDITION'
|
||||
@@ -337,10 +370,12 @@ async function fragment(context, source, rootOrderId, sequence) {
|
||||
}
|
||||
context.createdResources.phase3Loading = loading
|
||||
context.createdResources.orders = { ...(context.createdResources.orders || {}), ...loading,
|
||||
taxMode: 'BUY_DECLARATION' }
|
||||
taxMode: context.context.f40TaxMode }
|
||||
const palletLabelManifest = await verifyPalletLabels(context, loading, result.planId)
|
||||
check(context, 'F40 来源柜散货托盘码来自真实准备接口', Boolean(palletLabelManifest?.totalCount), `sequence=${sequence}`)
|
||||
for (const name of ['phase3-customs', 'service-center-flow', 'phase3-port-schedules',
|
||||
if(context.context.f40TaxMode==='TAX_REFUND') await prepareTaxCustoms(context,loading)
|
||||
else await stage(context,'phase3-customs')
|
||||
for (const name of ['service-center-flow', 'phase3-port-schedules',
|
||||
'phase3-outbound-dispatch', 'phase3-container-documents', 'phase3-carrier-schedule-queries']) await stage(context, name)
|
||||
const nodes = await import('./phase4-carrier-nodes.mjs')
|
||||
await nodes.runUntilPickupReady(context)
|
||||
@@ -379,11 +414,14 @@ async function sendFragment(context, producer, picker, merchantToken, index) {
|
||||
&& detail.skus?.length > 0 && detail.skus.every(row => row.inboundCode === producer.inboundCode)
|
||||
&& detail.skus.reduce((sum,row) => sum + Number(row.packageCount),0) === 1, `source=${producer.inboundCode}`)
|
||||
context.marker = `${context.context.f40BaseMarker}-SEND-${index}`
|
||||
const tripProof = await prepareFleetTrip(context, MODULE, dispatchNo, picker.warehouseId)
|
||||
check(context, 'F40 正式 PDA 拣货完成', await pickAll(context, picker.token, dispatchNo), `dispatch=${dispatchNo}`)
|
||||
const tripVersion = await confirmFleetLoading(context, MODULE, tripProof)
|
||||
check(context, 'F40 发车前置就绪', await verifyDepartureReadiness(context, picker.token, dispatchNo), `dispatch=${dispatchNo}`)
|
||||
const departed = await api(context, 'POST', 'F40 正式 PDA 发车', `${PDA}/${dispatchNo}/depart`,
|
||||
{ idempotencyKey: `${context.marker}-DEPART`, remark: context.marker }, picker.token)
|
||||
check(context, 'F40 正式发车已 DEPARTED', departed.status === 'DEPARTED', `dispatch=${dispatchNo}`)
|
||||
{ recordVersion: tripVersion, idempotencyKey: `${context.marker}-DEPART`.slice(0,128), remark: context.marker }, picker.token)
|
||||
check(context, 'F40 整车发车包含本片排车', departed.status === 'IN_TRANSIT' && departed.tripNo === tripProof.tripNo
|
||||
&& departed.dispatches?.some(item => item.dispatchNo === dispatchNo), `dispatch=${dispatchNo}`)
|
||||
const outbound = await api(context, 'GET', '回查实际来源出库完成',
|
||||
query(`/api/platform/v1/last-leg/outbound-orders/${transfer.outboundNo}`, { page:'1',size:'20' }))
|
||||
check(context,'F40 实际出库单 OUTBOUNDED 且来源入仓码匹配', outbound.outbound?.status === 'OUTBOUNDED'
|
||||
@@ -397,8 +435,9 @@ async function sendFragment(context, producer, picker, merchantToken, index) {
|
||||
const completed = await saveReceiptAndComplete(context, deliveries[0], customerId, index)
|
||||
check(context, 'F40 实际签收资料驱动派送完成', completed?.order?.status === 'DELIVERED', `delivery=${deliveries[0].deliveryNo}`)
|
||||
check(context,'F40 签收完成记录真实签收时间', Boolean(completed.order.signedAt), `delivery=${deliveries[0].deliveryNo}`)
|
||||
const tripClosure = await finishFleetTrip(context, MODULE, tripProof)
|
||||
return { producer, dispatchNo, outboundNo: transfer.outboundNo, deliveryNo: deliveries[0].deliveryNo,
|
||||
receiptVersion: completed.order.version, signedAt: completed.order.signedAt, receiptSnapshot:completed }
|
||||
receiptVersion: completed.order.version, signedAt: completed.order.signedAt, receiptSnapshot:completed, tripNo: tripProof.tripNo, tripClosure }
|
||||
}
|
||||
|
||||
/** Explicit TEST orchestrator, called by the parent acceptance context; never synthesizes an event. */
|
||||
@@ -426,11 +465,14 @@ async function runRootFulfillment(context) {
|
||||
['ROOT_FIRST','CHILD_FIRST','ROOT_SIGNED_REMAINDER_CANCELLED'].includes(signatureOrder), signatureOrder)
|
||||
const cancelRemainder = signatureOrder === 'ROOT_SIGNED_REMAINDER_CANCELLED'
|
||||
proof.signatureOrder = signatureOrder
|
||||
const taxMode=context.context?.f40TaxMode||'BUY_DECLARATION'
|
||||
check(context,'F40 税制必须为显式支持值',['BUY_DECLARATION','TAX_REFUND'].includes(taxMode),taxMode)
|
||||
proof.taxMode=taxMode;proof.taxCustoms=[]
|
||||
const scope = Object.create(context)
|
||||
scope.marker = `${context.marker}-F40`
|
||||
scope.context = { ...context.context, orderDestinationType: 'FBA', orderFulfillmentType: 'STANDARD',
|
||||
orderStopAtApproved:cancelRemainder,
|
||||
orderProfile: { taxMode: 'BUY_DECLARATION', fulfillmentType: 'STANDARD' },
|
||||
orderProfile: { taxMode, fulfillmentType: 'STANDARD' },f40TaxMode:taxMode,f40TaxEvidence:proof.taxCustoms,
|
||||
waitSplitVariant: { skuCount: 1, packageCountPerSku: 2, packageCounts: [2] },
|
||||
phase4OverseasPdaOperations: { completedFixtures: [] },
|
||||
allowAnyExistingPallet: true, f40BaseMarker: `${context.marker}-F40` }
|
||||
@@ -443,6 +485,7 @@ async function runRootFulfillment(context) {
|
||||
const profile = await api(scope,'GET','F40 本轮客户会话归属核验',
|
||||
'/api/merchant/v1/iam/tenant/profile',undefined,login.accessToken)
|
||||
const expectedCustomerId = id(profile.id)
|
||||
if(taxMode==='TAX_REFUND')scope.context.f40TaxMerchantToken=login.accessToken
|
||||
check(scope,'F40 客户会话和本轮客户 fixture 一致', Boolean(expectedCustomerId)
|
||||
&& (!credentials.customerId || credentials.customerId === expectedCustomerId),'exact-fixture-customer')
|
||||
await domesticLogin(scope)
|
||||
@@ -490,6 +533,9 @@ async function runRootFulfillment(context) {
|
||||
check(scope, 'F40 子单有唯一独立真实入仓来源', childRows.length === 1, `sources=${childRows.length}`)
|
||||
const childInbound = await api(scope, 'GET', '读取子单可装柜来源',
|
||||
query(`/api/platform/inbounds/${childRows[0].id}`, { tenantId: customerId }))
|
||||
proof.remainderTaxIdentity = {taxMode,customerId,rootOrderId,childOrderId,rootOrderNo:orderFixture.orderNo,
|
||||
rawChild:childDetail.order,rootInbound:rootInbound.summary,childInbound:childInbound.summary}
|
||||
await verifyRemainderTaxIdentity(scope,proof.remainderTaxIdentity,login.accessToken)
|
||||
let second = null
|
||||
if (cancelRemainder) {
|
||||
check(scope,'F40 取消专项真实余量未装且原始 APPROVED',
|
||||
@@ -628,6 +674,7 @@ async function runRootFulfillment(context) {
|
||||
'exact-root/source/customer/readOnly-proof')
|
||||
check(scope,'F40 新键重复签收完成Outbox数量与发布状态零漂移',
|
||||
outboxCounts(proof.beforeReceiptReplayOutbox) === outboxCounts(proof.outboxProof),'before-after-receipt-replay-outbox')
|
||||
if(taxMode==='TAX_REFUND')await verifyTaxDocumentsAfterCompletion(scope,proof.taxCustoms,login.accessToken)
|
||||
proof.finalStatus='VERIFIED'
|
||||
} else {
|
||||
proof.finalStatus='HTTP_VERIFIED_OUTBOX_PENDING'
|
||||
|
||||
Reference in New Issue
Block a user