test(fba): preserve verified fleet and fulfillment acceptance workflows

This commit is contained in:
2026-10-03 06:26:33 +08:00
parent c55bd7f4b6
commit d858357c5d
52 changed files with 4217 additions and 103 deletions
@@ -229,7 +229,7 @@ async function verifyTransfer(context, token, orders) {
return verifyWarehouseTransferReview(context, token, order.row.customerId, body, transferNo)
}
async function verifyDiscard(context, token, markerOrders) {
export async function verifyDiscard(context, token, markerOrders) {
const response = await context.request('GET', `${DISCARD}/inventory-candidates`, undefined, token, {})
const candidates = context.apiSuccess(MODULE, '客户查询货物废弃库存候选', response, trace(response), OPTIONS)
const markerOrderIds = new Set(markerOrders.map((value) => id(value.row?.id)))
@@ -245,6 +245,14 @@ async function verifyDiscard(context, token, markerOrders) {
items: [{ orderId: id(candidate.orderId), skuCode: sku.skuCode, discardPackages: 1 }],
idempotencyKey: `${context.marker}-CLIENT-DISCARD`.slice(0, 255), remark: context.marker,
}
const proof = context.createdResources.phase4OverseasClientServices.discardApproval = {
sourceOrderId: id(candidate.orderId), customerId: id(context.context.merchantQualificationFixture.tenantId),
warehouseId: candidate.overseasWarehouseId, inboundCode: candidate.inboundCode,
skuCode: sku.skuCode, snapshots: {}, status: 'RUNNING',
contract: 'Current stage-five and service AGENTS require approval-transaction deduction. Review has no idempotencyKey. An approval that only enables PDA must fail this contract.',
}
const before = await discardSnapshot(context, token, proof)
proof.snapshots.before = before
const created = await merchant(context, token, 'POST', '客户创建货物废弃申请', DISCARD, body)
const discardNo = id(created.data?.summary?.discardNo)
context.assert(MODULE, '废弃申请只锁定库存并进入合法待审核状态', created.response,
@@ -267,14 +275,97 @@ async function verifyDiscard(context, token, markerOrders) {
&& Array.isArray(detail.data?.histories) && detail.data.histories.some((item) => item?.toStatus === 'PENDING_REVIEW'),
`items=${detail.data?.items?.length ?? 'missing'}, histories=${detail.data?.histories?.length ?? 'missing'}; ${trace(detail.response)}`, OPTIONS)
context.createdResources.phase4OverseasClientServices.discardNo = discardNo
try { await verifyDiscardApproval(context, token, body, discardNo, proof) }
catch (error) { proof.status = 'FAILED'; proof.failure = error.message; throw error }
return true
}
function stable(value) {
if (Array.isArray(value)) return JSON.stringify(value.map(stable).sort())
if (value && typeof value === 'object') return JSON.stringify(Object.fromEntries(Object.keys(value).sort().map(k => [k, stable(value[k])])))
return value
}
function discardRequire(context, name, passed, evidence = '') {
context.assert(MODULE, name, null, Boolean(passed), evidence, OPTIONS)
if (!passed) throw new Error(name)
}
async function discardCommand(context, token, method, name, path, body) {
const result = await merchant(context, token, method, name, path, body)
if (!success(result.response)) throw new Error(`${name}: ${trace(result.response)}`)
return result.data
}
async function discardSnapshot(context, token, proof, discardNo) {
const all = async (kind) => {
const rows = []
let total
for (let page = 1; page <= 100; page++) {
const data = await discardCommand(context, context.token, 'GET', `废弃核对完整${kind}`,
query(`/api/platform/v1/last-leg/inventory/${kind}`, { customerId: proof.customerId,
overseasWarehouseId: proof.warehouseId, page, size: 200 }))
discardRequire(context, '废弃库存分页作用域与总数稳定', Array.isArray(data?.records)
&& Number.isSafeInteger(Number(data.total)) && Number(data.total) >= 0
&& (total === undefined || total === Number(data.total))
&& data.records.every(row => id(row.customerId) === proof.customerId && row.overseasWarehouseId === proof.warehouseId))
total = Number(data.total); rows.push(...data.records)
if (rows.length === total) return rows
discardRequire(context, '废弃库存分页不能截断', rows.length < total && data.records.length > 0)
}
throw new Error('废弃库存分页超过上限')
}
const balances = await all('balances'), ledger = await all('ledger')
const candidates = await discardCommand(context, token, 'GET', '废弃回查本轮客户真实候选', `${DISCARD}/inventory-candidates`)
const source = candidates.find(row => id(row.orderId) === proof.sourceOrderId)
const detail = discardNo ? await discardCommand(context, token, 'GET', '废弃回查申请真实状态', `${DISCARD}/${encodeURIComponent(discardNo)}`) : null
return { balances, ledger, source: source || null, detail }
}
export async function verifyDiscardApproval(context, token, body, discardNo, proof) {
const require = (name, passed) => discardRequire(context, name, passed, `discardNo=${discardNo}`)
const sameStock = (a,b) => stable(a.balances) === stable(b.balances) && stable(a.ledger) === stable(b.ledger)
const base = proof.snapshots.before
require('废弃来源属于本轮客户及实际收货仓', base.source?.overseasWarehouseId === proof.warehouseId
&& base.source?.inboundCode === proof.inboundCode && Number(base.source.availablePackages) >= 1)
let pending = await discardSnapshot(context, token, proof, discardNo)
proof.snapshots.pending = pending
require('申请仅锁定一箱且余额流水未扣减', sameStock(base,pending)
&& Number(pending.source?.availablePackages || 0) === Number(base.source.availablePackages) - 1)
const conflictBody = { ...body, remark: `${body.remark}-DIFFERENT` }
const response = await context.request('POST', DISCARD, conflictBody, token, {})
context.addStep(MODULE, '废弃创建同键异载荷拒绝', response, conflict(response), trace(response), OPTIONS)
require('废弃创建异载荷有明确冲突诊断', conflict(response) && nonBlank(response.requestId) && nonBlank(response.traceId))
require('废弃创建异载荷拒绝后事实不变', stable(await discardSnapshot(context,token,proof,discardNo)) === stable(pending))
const root = '/api/platform/v1/last-leg/cargo-discards'
const review = (number,decision) => discardCommand(context, context.token, 'POST', `废弃审批${decision}`,
query(`${root}/${encodeURIComponent(number)}/review`, { customerId: proof.customerId }), { decision, remark: context.marker })
await review(discardNo,'REJECT')
const rejected = await discardSnapshot(context,token,proof,discardNo); proof.snapshots.rejected = rejected
require('废弃拒绝释放原箱且全部库存数量流水不变', rejected.detail?.summary?.status === 'REJECTED'
&& sameStock(base,rejected) && stable(base.source) === stable(rejected.source))
await review(discardNo,'REJECT')
require('废弃重复拒绝不重复释放', stable(await discardSnapshot(context,token,proof,discardNo)) === stable(rejected))
const approvedBody = { ...body, idempotencyKey: `${body.idempotencyKey}-APPROVE` }
const created = await discardCommand(context,token,'POST','创建独立待批准废弃申请',DISCARD,approvedBody)
const approvedNo = id(created?.summary?.discardNo); require('批准对照使用本轮新申请', nonBlank(approvedNo) && approvedNo !== discardNo)
proof.approvedNo = approvedNo
await review(approvedNo,'APPROVE')
const approved = await discardSnapshot(context,token,proof,approvedNo); proof.snapshots.approved = approved
const available = snapshot => snapshot.balances.reduce((sum,row)=>sum+Number(row.availablePackages),0)
require('正式废弃合同要求审批事务扣减一箱', approved.detail?.summary?.status === 'APPROVED' && available(base)-available(approved) === 1)
await review(approvedNo,'APPROVE')
require('重复批准不会提前扣库存或新增历史', stable(await discardSnapshot(context,token,proof,approvedNo)) === stable(approved))
const reverse = await context.request('POST',query(`${root}/${encodeURIComponent(approvedNo)}/review`,{customerId:proof.customerId}),{decision:'REJECT',remark:context.marker},context.token,{})
require('批准后反向审核拒绝且事实不变', conflict(reverse) && nonBlank(reverse.requestId) && nonBlank(reverse.traceId)
&& stable(await discardSnapshot(context,token,proof,approvedNo)) === stable(approved))
proof.status = 'APPROVAL_HTTP_VERIFIED_CARTON_SQL_PENDING'
proof.remainingEvidence = 'll_dropship_carton, ll_dropship_sku_inventory, ll_dropship_carton_ledger require independent read-only evidence; public ledger is not carton ledger'
}
export async function run(context) {
context.createdResources.phase4OverseasClientServices = {
marker: context.marker, relabelNo: null, transferNo: null, discardNo: null,
finalStatuses: { relabel: null, transfer: null, discard: null },
cleanup: 'Relabel/discard remain pending; transfers are reviewed with separate incoming approval/rejection fixtures; no outbound, dispatch, PDA or delivery action is invoked',
cleanup: 'Relabel remains pending; transfer review fixtures retained; discard rejection and approval-transaction deduction checked; no outbound, dispatch or delivery action is invoked',
}
const token = await loginMerchant(context)
if (!token) return
@@ -300,6 +391,6 @@ export async function run(context) {
context.createdResources.phase4OverseasClientServices.finalStatuses = {
relabel: relabelDone ? 'PENDING_REVIEW' : null,
transfer: transferDone ? 'APPROVED' : null,
discard: discardDone ? 'PENDING_REVIEW' : null,
discard: discardDone ? 'APPROVED' : null,
}
}