This commit is contained in:
2026-09-30 17:13:11 +08:00
parent babc7de4b6
commit 32fb9ec7b3
26 changed files with 1484 additions and 70 deletions
+99 -17
View File
@@ -42,6 +42,61 @@ export function fixtureInboundCode(policy, marker) {
// Short enough for the 34-character carton QR limit; stable within this fresh fixture.
return { inboundCode: `AT${createHash('sha256').update(marker).digest('hex').slice(0, 20).toUpperCase()}` }
}
export function cargoRequirementNegativeCases(payload) {
const cases = [
{ name: '木制品未选择商检', at: 'CREATE', mutate: value => { value.customsInspection = false; value.cargoes[0].woodenProduct = true } },
{ name: '货物零箱数', at: 'CREATE', mutate: value => { value.cargoes[0].packageCount = 0 } },
{ name: '货物总件数与每箱件数不守恒', at: 'CREATE', mutate: value => { value.cargoes[0].totalQuantity++ } },
]
if (payload.destinationType === 'FBA') {
cases.push({ name: 'FBA缺少订单货件编号', at: 'SUBMIT', mutate: value => { value.shipmentId = null } },
{ name: 'FBA缺少逐行跟踪编号', at: 'SUBMIT', mutate: value => { value.cargoes[0].referenceId = null } })
}
if (payload.fulfillmentType === 'ONE_CLICK_DROPSHIP') {
cases.push({ name: '一件代发缺少SKU', at: 'CREATE_OR_SUBMIT', mutate: value => { value.cargoes[0].skuNo = null } },
{ name: '一件代发混箱组箱规冲突', at: 'SUBMIT', mutate: value => {
const extra = structuredClone(value.cargoes[0]); extra.skuNo = `${extra.skuNo}-B`.slice(0, 64); extra.lengthCm++
value.cargoes.push(extra)
} })
}
return cases.map(({ mutate, ...item }) => { const body = structuredClone(payload); mutate(body); return { ...item, body } })
}
async function verifyCargoRequirementNegatives(context, payload, policy, merchantToken) {
const base = '/api/merchant/v1/orders/first-leg'
const cases = cargoRequirementNegativeCases(payload)
const evidence = []
for (const [index, item] of cases.entries()) {
const body = { ...item.body, ...fixtureInboundCode(policy, `${context.marker}-NEG-${index}`) }
const created = await request(context, 'POST', base, body, merchantToken,
{ 'Idempotency-Key': key(context, `cargo-negative-${index}-create`) })
if (item.at === 'CREATE' || String(created?.body?.code) !== '200') {
context.assert(MODULE, `${payload.destinationType}/${payload.fulfillmentType} ${item.name}创建拒绝`, created,
created.http === 200 && String(created.body?.code) === 'A0422',
`HTTP=${created.http}, code=${created.body?.code || 'missing'}`, CONTRACT_OPTIONS)
evidence.push({ name: item.name, stage: 'CREATE', code: created.body?.code,
orderId: String(created.body?.code) === '200' ? id(created.body?.data) : null })
continue
}
const orderId = id(created.body?.data)
if (!orderId) throw new Error('negative draft missing order id')
const before = await request(context, 'GET', `${base}/${orderId}`, undefined, merchantToken)
const submitted = await request(context, 'POST', `${base}/${orderId}/submit`, undefined, merchantToken,
{ 'Idempotency-Key': key(context, `cargo-negative-${index}-submit`) })
const after = await request(context, 'GET', `${base}/${orderId}`, undefined, merchantToken)
context.assert(MODULE, `${payload.destinationType}/${payload.fulfillmentType} ${item.name}提交拒绝且草稿零漂移`, submitted,
submitted.http === 200 && String(submitted.body?.code) === 'A0422'
&& String(before.body?.code) === '200' && String(after.body?.code) === '200'
&& after.body.data?.order?.status === 'DRAFT'
&& JSON.stringify(before.body.data) === JSON.stringify(after.body.data),
`orderId=${orderId}, code=${submitted.body?.code || 'missing'}`, CONTRACT_OPTIONS)
evidence.push({ name: item.name, stage: 'SUBMIT', code: submitted.body?.code, orderId })
}
context.createdResources.orderCargoNegatives ??= []
context.createdResources.orderCargoNegatives.push({ marker: context.marker,
destinationType: payload.destinationType, fulfillmentType: payload.fulfillmentType, cases: evidence })
}
function declarationCargo(source, index) {
const number = (value) => Number.isFinite(Number(value)) ? Number(value) : 0
const optionalNumber = (value) => value === null || value === undefined || value === '' ? null : number(value)
@@ -152,6 +207,33 @@ export async function verifyOrderApprovalReplay(context, { tenantId, orderId, in
context.createdResources.orders.approvalReplay = evidence
return evidence
}
export async function finishOrderLifecycle(context, { tenantId, orderId, firstLegDetailPath }) {
const stopAtApproved = context.context?.orderStopAtApproved === true
if (!stopAtApproved) {
const acceptResponse = await context.request('POST',
`/api/platform/v1/orders/first-leg/${encodeURIComponent(orderId)}/accept?tenantId=${encodeURIComponent(tenantId)}`)
context.apiSuccess(MODULE, 'Admin 受理本轮订单', acceptResponse)
const startResponse = await context.request('POST',
`/api/platform/v1/orders/first-leg/${encodeURIComponent(orderId)}/start?tenantId=${encodeURIComponent(tenantId)}`)
context.apiSuccess(MODULE, 'Admin 启动本轮订单履约', startResponse)
}
const finalDetailResponse = await context.request('GET', firstLegDetailPath)
const finalDetail = context.apiSuccess(MODULE, 'Admin 回查本轮订单履约状态', finalDetailResponse)
const history = finalDetail?.statusHistory
const valid = finalDetail?.order?.status === (stopAtApproved ? 'APPROVED' : 'PROCESSING')
&& history?.some?.((item) => item.toStatus === 'APPROVED')
&& (stopAtApproved
? !history.some((item) => ['ACCEPTED', 'PROCESSING'].includes(item.toStatus))
: history.some((item) => item.toStatus === 'ACCEPTED')
&& history.some((item) => item.toStatus === 'PROCESSING'))
context.assert(MODULE, stopAtApproved ? '显式审批停点保留原始APPROVED且未受理启动' : '本轮订单已进入处理中且保留状态历史',
finalDetailResponse, valid,
`status=${finalDetail?.order?.status ?? 'missing'}, history=${history?.length ?? 'n/a'}`, CONTRACT_OPTIONS)
if (stopAtApproved && !valid) throw new Error('显式审批停点未保留原始APPROVED,不能继续准备该专项')
context.createdResources.orders.rawOrderStatus = finalDetail?.order?.status || null
context.createdResources.orders.stoppedAtApproved = stopAtApproved
}
function tomorrow(days = 30) {
const date = new Date(); date.setUTCDate(date.getUTCDate() + days)
return date.toISOString().slice(0, 10)
@@ -239,6 +321,14 @@ export async function findPricedOrderRouteAcrossOrigins({ receivings, destinatio
traceId: channelResponse?.traceId ?? 'missing', count: channels.length })
continue
}
if (destinationType === 'PRIVATE') {
// Private destinations use customer-owned addresses, not warehouse-rate
// destination rows. Match the same authoritative channel path already
// used by fixed-origin private orders.
const channel = priceableChannels[0]
return { receiving, priced: { channel, pricingTaxMode: channel.pricingTaxModes[0], destination: null },
response: channelResponse, attempts: 0, failures: [], originFailures }
}
const result = await findPricedOrderRoute({ receiving, destinations, channels: priceableChannels,
estimate, concurrency, destinationType })
attempts += result.attempts
@@ -439,7 +529,9 @@ async function loadMasterData(context, merchantToken) {
}
const priced = pricingResult.priced
const receiving = pricingResult.receiving
context.addStep(MODULE, 'Client 从 ACTIVE 国内仓选择具有真实价格覆盖的渠道和目的仓', pricingResult.response, Boolean(priced),
context.addStep(MODULE, destinationType === 'PRIVATE'
? 'Client 私人地址订单选择有效国内仓渠道与报价税制'
: 'Client 从 ACTIVE 国内仓选择具有真实价格覆盖的渠道和目的仓', pricingResult.response, Boolean(priced),
priced
? `origin=${receiving.id}, priceBookVersionId=${priced.channel.priceBookVersionId}, channel=${priced.channel.channelCode}, pricingTaxMode=${priced.pricingTaxMode}, destination=${priced.destination?.warehouseCode || 'PRIVATE_ADDRESS'}, attempts=${pricingResult.attempts}`
: originPricingFailureEvidence(pricingResult),
@@ -559,7 +651,8 @@ function buildPayload(context, profile, master, productImageFileId, customerAddr
shipmentId: master.destinationType === 'FBA' ? shipmentId : null,
referenceId: master.destinationType === 'FBA' ? referenceId : null,
upcNo: `${Date.now()}${index}`.slice(-12),
productImageFileId, dangerousGoods: false, woodenProduct: false,
productImageFileId, dangerousGoods: orderProfile.dangerousGoods === true,
woodenProduct: orderProfile.woodenProduct === true,
cargoRemark: `${context.marker} boxType=${box.boxType}`,
}
}),
@@ -673,6 +766,9 @@ export async function run(context) {
const policy = context.apiSuccess(MODULE, 'Client 读取入仓码录入策略', policyResponse, '', CONTRACT_OPTIONS)
const payload = { ...buildPayload(context, profile, master, productImageFileId, customerAddresses),
...fixtureInboundCode(policy, context.marker) }
if (context.context.verifyOrderCargoNegatives === true) {
await verifyCargoRequirementNegatives(context, payload, policy, merchantToken)
}
const createResponse = await request(context, 'POST', createPath, payload, merchantToken,
{ 'Idempotency-Key': key(context, 'client-create') })
const orderId = id(context.apiSuccess(MODULE, 'Client 创建四箱型隔离订单', createResponse,
@@ -910,19 +1006,5 @@ export async function run(context) {
`taskId=${task.id}, total=${history?.total ?? 'n/a'}`)
}
const acceptResponse = await context.request('POST',
`/api/platform/v1/orders/first-leg/${encodeURIComponent(orderId)}/accept?tenantId=${encodeURIComponent(tenantId)}`)
context.apiSuccess(MODULE, 'Admin 受理本轮订单', acceptResponse)
const startResponse = await context.request('POST',
`/api/platform/v1/orders/first-leg/${encodeURIComponent(orderId)}/start?tenantId=${encodeURIComponent(tenantId)}`)
context.apiSuccess(MODULE, 'Admin 启动本轮订单履约', startResponse)
const finalDetailResponse = await context.request('GET', firstLegDetailPath)
const finalDetail = context.apiSuccess(MODULE, 'Admin 回查本轮订单履约状态', finalDetailResponse)
context.assert(MODULE, '本轮订单已进入处理中且保留状态历史', finalDetailResponse,
finalDetail?.order?.status === 'PROCESSING'
&& finalDetail?.statusHistory?.some?.((item) => item.toStatus === 'APPROVED')
&& finalDetail?.statusHistory?.some?.((item) => item.toStatus === 'ACCEPTED')
&& finalDetail?.statusHistory?.some?.((item) => item.toStatus === 'PROCESSING'),
`status=${finalDetail?.order?.status ?? 'missing'}, history=${finalDetail?.statusHistory?.length ?? 'n/a'}`)
await finishOrderLifecycle(context, { tenantId, orderId, firstLegDetailPath })
}