Files
baiyue-firstleg/scripts/admin-test-env-modules/phase4-overseas-wallet-and-pallet-cost.mjs
T

378 lines
26 KiB
JavaScript

const MODULE = 'phase4-overseas-wallet-and-pallet-cost'
const SCENARIO = 'phase4-overseas-wallet-and-pallet-cost-http'
const OPTIONS = { scenario: SCENARIO, deploymentBlocker: true }
const PROCUREMENT = '/api/platform/v1/last-leg/overseas-procurements'
const RELABEL = '/api/platform/v1/last-leg/relabel-applications'
const FINANCE = '/api/platform/finance/v1'
const CLIENT_FINANCE = '/api/client/finance/v1'
const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64')
const id = (value) => value == null ? '' : String(value)
const money = (value) => Number(Number(value || 0).toFixed(2))
const success = (response) => response?.http === 200 && String(response?.body?.code) === '200'
const rows = (value) => Array.isArray(value) ? value : Array.isArray(value?.records) ? value.records : []
const trace = (response) => `HTTP=${response?.http ?? 'missing'}, code=${response?.body?.code ?? 'missing'}, requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}`
const query = (path, params) => {
const search = new URLSearchParams()
for (const [key, value] of Object.entries(params)) if (value !== undefined && value !== null && value !== '') search.set(key, String(value))
return `${path}?${search}`
}
export function beijingDate(now = new Date()) {
const parts = Object.fromEntries(new Intl.DateTimeFormat('en-US', {
timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit',
}).formatToParts(now).map((part) => [part.type, part.value]))
return `${parts.year}${parts.month}${parts.day}`
}
export function matchesProcurementNo(orderNo, prefix, now = new Date()) {
return typeof prefix === 'string' && prefix.length > 0
&& new RegExp(`^${prefix}${beijingDate(now)}\\d{4}$`).test(String(orderNo || ''))
}
async function poll(action, predicate, attempts = 60, intervalMs = 1_000) {
let result = null
for (let index = 0; index < attempts; index += 1) {
result = await action()
if (predicate(result)) return { result, attempts: index + 1 }
if (index + 1 < attempts) await new Promise((resolve) => setTimeout(resolve, intervalMs))
}
return { result, attempts }
}
function assert(context, name, response, passed, evidence = '') {
context.assert(MODULE, name, response, passed, `${evidence}${evidence ? '; ' : ''}${trace(response)}`, OPTIONS)
}
async function merchantLogin(context, fixture) {
const response = await context.request('POST', '/api/auth/login', { loginId: fixture.username, password: fixture.password }, '')
const auth = success(response) ? response.body.data : null
assert(context, '客户账号同时取得国内和海外短时Token', response,
Boolean(auth?.accessToken) && Boolean(auth?.accessToken),
`customerId=${fixture.tenantId}, domestic=${Boolean(auth?.accessToken)}, overseas=${Boolean(auth?.accessToken)}`)
return auth
}
async function platformAccount(context, customerId) {
const response = await context.request('GET', query(`${FINANCE}/accounts`, { page: 1, pageSize: 20, customerId }))
const account = success(response) ? rows(response.body.data).find((item) => id(item.customerId) === id(customerId)) : null
return { response, account }
}
async function prepareWallet(context, customerId) {
const before = await platformAccount(context, customerId)
assert(context, '平台客户账户返回余额和授信字段', before.response,
before.account && ['actualBalance', 'frozenBalance', 'availableBalance', 'creditLimit', 'creditUsed', 'availableCredit', 'accountVersion']
.every((field) => Number.isFinite(Number(before.account[field]))), `customerId=${customerId}`)
if (!before.account) return null
const creditLimit = 2.00
const creditExpiresOn = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10)
const credit = await context.request('PUT', `${FINANCE}/accounts/${encodeURIComponent(customerId)}/credit-limit`,
{ creditLimit, creditExpiresOn, remark: context.marker,
expectedAccountVersion: Number(before.account.accountVersion) }, context.token,
{ 'Idempotency-Key': `${context.marker}-wallet-credit-limit`.slice(0, 128) })
assert(context, '授信额度可通过正式接口初始化', credit,
success(credit) && money(credit.body.data?.creditLimitAfter) === creditLimit,
`creditLimit=${credit.body?.data?.creditLimitAfter ?? 'missing'}`)
if (!success(credit)) return null
const credited = await platformAccount(context, customerId)
assert(context, '授信初始化后账户版本和可用授信已更新', credited.response,
credited.account && money(credited.account.creditLimit) === creditLimit
&& Number(credited.account.accountVersion) > Number(before.account.accountVersion),
`version=${credited.account?.accountVersion ?? 'missing'}, availableCredit=${credited.account?.availableCredit ?? 'missing'}`)
if (!credited.account) return null
const rechargeAmount = 10.00
const recharge = await context.request('POST', `${FINANCE}/accounts/${encodeURIComponent(customerId)}/adjustments`, {
type: 'RECHARGE', amount: rechargeAmount, occurredDate: new Date().toISOString().slice(0, 10),
paymentMethod: 'BANK_TRANSFER', referenceNo: `${context.marker}-AUTO`, remark: context.marker,
expectedAccountVersion: Number(credited.account.accountVersion),
}, context.token, { 'Idempotency-Key': `${context.marker}-wallet-recharge`.slice(0, 128) })
assert(context, '平台人工充值立即进入实际余额', recharge,
success(recharge) && money(recharge.body.data?.actualBalanceAfter) === money(Number(before.account.actualBalance) + rechargeAmount),
`before=${before.account.actualBalance}, after=${recharge.body?.data?.actualBalanceAfter ?? 'missing'}`)
return success(recharge) ? { before: before.account, funded: recharge.body.data, rechargeAmount, creditLimit } : null
}
async function procurementFlow(context, fixture, token) {
const merchantPath = '/api/merchant/v1/last-leg/overseas-procurements'
const payload = {
customerName: fixture.tenantName, purchaseCategory: 'OFFICE_SUPPLIES', productName: `${context.marker} 钱包采购`,
productSpecification: 'full acceptance wallet', productUrl: null, purchaseQuantity: 2,
budgetUnitPrice: 1, currency: 'USD', receivingAddress: 'Los Angeles',
contactName: '全量验收联系人', contactPhone: '13800000002', remark: context.marker,
idempotencyKey: `${context.marker}-wallet-procurement`.slice(0, 255),
}
const created = await context.request('POST', merchantPath, payload, token, {})
const detail = created?.body?.data
const orderNo = detail?.summary?.orderNo
assert(context, '客户创建钱包采购单', created,
success(created) && id(detail?.summary?.customerId) === id(fixture.tenantId) && detail?.summary?.status === 'PENDING_REVIEW',
`orderNo=${orderNo || 'missing'}, status=${detail?.summary?.status || 'missing'}`)
if (!success(created) || !orderNo) return null
assert(context, '采购单号按客户拼音首字母加北京时间日期和四位序号生成', created,
fixture.expectedProcurementPrefix ? matchesProcurementNo(orderNo, fixture.expectedProcurementPrefix) : /^[A-Z0-9]{1,8}\d{8}\d{4}$/.test(orderNo),
`orderNo=${orderNo}, expectedPrefix=${fixture.expectedProcurementPrefix || 'custom-customer'}`)
const customerId = id(fixture.tenantId)
const approved = await context.request('POST', query(`${PROCUREMENT}/${encodeURIComponent(orderNo)}/review`, { customerId }), {
version: Number(detail.summary.version), decision: 'APPROVE',
idempotencyKey: `${context.marker}-wallet-procurement-review`.slice(0, 255), remark: context.marker,
})
assert(context, '平台审核通过钱包采购单', approved,
success(approved) && approved.body.data?.summary?.status === 'APPROVED',
`status=${approved.body?.data?.summary?.status || 'missing'}`)
if (!success(approved)) return null
const files = []
for (const category of ['VOUCHER', 'RECEIPT']) {
const uploaded = await context.requestMultipart('POST',
query(`${PROCUREMENT}/${encodeURIComponent(orderNo)}/files/${category}`, { customerId }),
{ content: PNG, type: 'image/png', name: `${category.toLowerCase()}-${orderNo}.png` })
assert(context, `采购单上传${category}文件`, uploaded,
success(uploaded) && id(uploaded.body.data?.fileId) !== '' && uploaded.body.data?.category === category,
`fileId=${id(uploaded.body?.data?.fileId) || 'missing'}`)
if (success(uploaded)) files.push(uploaded.body.data)
}
if (files.length !== 2) return null
const fee = await context.request('POST', query(`${PROCUREMENT}/${encodeURIComponent(orderNo)}/fees`, { customerId }), {
feeItemCode: 'PROCUREMENT_SERVICE', feeItemName: '采购服务费', amount: 0.01,
voucherFileId: id(files.find((item) => item.category === 'VOUCHER')?.fileId), remark: context.marker,
})
const feeNo = fee?.body?.data?.feeNo
assert(context, '采购费用创建并开始钱包冻结', fee,
success(fee) && Boolean(feeNo) && ['FREEZE_PENDING', 'FROZEN'].includes(fee.body.data?.walletStatus),
`feeNo=${feeNo || 'missing'}, walletStatus=${fee.body?.data?.walletStatus || 'missing'}`)
if (!success(fee) || !feeNo) return null
const detailPath = query(`${PROCUREMENT}/${encodeURIComponent(orderNo)}`, { customerId })
const frozen = await poll(async () => context.request('GET', detailPath),
(response) => success(response) && response.body.data?.fees?.find((item) => item.feeNo === feeNo)?.walletStatus === 'FROZEN')
const frozenFee = frozen.result?.body?.data?.fees?.find((item) => item.feeNo === feeNo)
assert(context, '采购费用跨区冻结到账', frozen.result,
frozenFee?.walletStatus === 'FROZEN', `feeNo=${feeNo}, pollRequests=${frozen.attempts}, walletStatus=${frozenFee?.walletStatus || 'missing'}`)
if (frozenFee?.walletStatus !== 'FROZEN') return null
const current = frozen.result.body.data.summary
const settledOrder = await context.request('PATCH', detailPath, {
version: Number(current.version), inboundQuantity: Number(current.purchaseQuantity),
})
assert(context, '采购全量入库推进待出库并触发结算', settledOrder,
success(settledOrder) && settledOrder.body.data?.summary?.status === 'PENDING_OUTBOUND',
`status=${settledOrder.body?.data?.summary?.status || 'missing'}`)
const settled = await poll(async () => context.request('GET', detailPath),
(response) => success(response) && response.body.data?.fees?.find((item) => item.feeNo === feeNo)?.walletStatus === 'SETTLED')
const settledFee = settled.result?.body?.data?.fees?.find((item) => item.feeNo === feeNo)
assert(context, '采购费用跨区结算到账', settled.result,
settledFee?.walletStatus === 'SETTLED', `feeNo=${feeNo}, pollRequests=${settled.attempts}, walletStatus=${settledFee?.walletStatus || 'missing'}`)
return { orderNo, feeNo, freezePollRequests: frozen.attempts, settlePollRequests: settled.attempts }
}
function relabelPayload(context, warehouseId, source, suffix, chargeAmount) {
const value = String(context.marker).replace(/[^A-Za-z0-9]/g, '').slice(-18).toUpperCase()
return {
customerOrderNo: `${context.marker}-${suffix}`.slice(0, 128), operationWarehouseId: id(warehouseId),
labelDeadline: new Date(Date.now() + 86_400_000).toISOString().slice(0, 19),
contactName: '全量验收联系人', contactPhone: '+1-213-555-0199', chargeAmount,
deliveryMethod: 'TRUCK', printShippingLabel: false,
remark: context.marker, idempotencyKey: `${context.marker}-${suffix}`.slice(0, 255),
items: [{ sourceInventoryBalanceId: id(source.inventoryBalanceId),
sourceInventoryPositionId: id(source.position.positionId), sourceInboundOrderId: id(source.inboundOrderId),
skuCode: source.skuCode || `OLD-${value}-${suffix}`.slice(0, 128),
productNameZh: source.productNameZh || `${context.marker} 换标商品`,
totalPackageCount: 1, notifiedPackageCount: 1, shipmentNo: `SHIP-${value}-${suffix}`.slice(0, 128),
newSkuCode: `NEW-${value}-${suffix}`.slice(0, 128), labelRequirement: context.marker,
cartonMark: `CM-${value}-${suffix}`.slice(0, 255),
fbaCartonMark: `FBA-${value}-${suffix}`.slice(0, 255),
destinationFbaWarehouseCode: source.destinationFbaWarehouseCode }],
}
}
async function relabelFlow(context, token, warehouseId, suffix, terminalStatus) {
const candidatesResponse = await context.request('GET', query(
'/api/merchant/v1/last-leg/relabel-applications/inventory-candidates', { operationWarehouseId: warehouseId }), undefined, token, {})
const candidates = success(candidatesResponse) && Array.isArray(candidatesResponse.body?.data)
? candidatesResponse.body.data : []
const currentPda = context.createdResources?.phase4OverseasPda
const protectedPalletIds = new Set([
...(Array.isArray(currentPda?.palletIds) ? currentPda.palletIds : []),
...Object.values(currentPda?.generatedPalletsByInbound || {})
.flatMap((items) => Array.isArray(items) ? items.map((item) => id(item?.palletId)) : []),
].filter(Boolean))
const producerFixtures = context.createdResources?.phase4InboundProducerFixtures
const protectedInboundCodes = new Set((producerFixtures?.containers || [])
.map((item) => id(item?.inboundCode)).filter(Boolean))
const protectedInboundIds = new Set((producerFixtures?.containers || [])
.flatMap((item) => [item?.inboundId, item?.sourceInboundId, item?.inboundOrderId].map(id))
.filter(Boolean))
const isProtected = (candidate) => {
if (protectedInboundIds.has(id(candidate?.inboundOrderId))
|| protectedInboundCodes.has(id(candidate?.inboundCode))) return true
return (Array.isArray(candidate?.positions) ? candidate.positions : []).some((position) =>
protectedPalletIds.has(id(position?.palletId))
|| protectedPalletIds.has(id(position?.pallet?.id))
|| protectedPalletIds.has(id(position?.pallet?.palletId))
|| [...protectedPalletIds].includes(id(position?.sourcePalletId)))
}
const source = candidates.find((candidate) => id(candidate.inboundOrderId)
&& id(candidate.inventoryBalanceId)
&& id(candidate.destinationFbaWarehouseCode)
&& !isProtected(candidate)
&& candidate.positions?.some((position) => id(position.positionId) && Number(position.availablePackages) > 0))
if (source) source.position = source.positions.find((position) => id(position.positionId)
&& Number(position.availablePackages) > 0)
assert(context, `${suffix}换标读取当前客户权威库存候选`, candidatesResponse,
Boolean(source?.position), `warehouseId=${warehouseId}, candidates=${candidates.length}`)
if (!source?.position) return null
const payload = relabelPayload(context, warehouseId, source, suffix, 0.01)
const created = await context.request('POST', '/api/merchant/v1/last-leg/relabel-applications', payload, token, {})
const relabelNo = created?.body?.data?.application?.relabelNo
assert(context, `${suffix}换标单创建并携带收费金额`, created,
success(created) && Boolean(relabelNo) && money(created.body.data?.application?.chargeAmount) === 0.01,
`relabelNo=${relabelNo || 'missing'}, charge=${created.body?.data?.application?.chargeAmount ?? 'missing'}`)
if (!success(created) || !relabelNo) return null
const detailPath = `${RELABEL}/${encodeURIComponent(relabelNo)}`
const frozen = await poll(async () => context.request('GET', detailPath),
(response) => success(response) && response.body.data?.application?.walletStatus === 'FROZEN')
assert(context, `${suffix}换标费用跨区冻结到账`, frozen.result,
frozen.result?.body?.data?.application?.walletStatus === 'FROZEN',
`relabelNo=${relabelNo}, pollRequests=${frozen.attempts}, walletStatus=${frozen.result?.body?.data?.application?.walletStatus || 'missing'}`)
if (frozen.result?.body?.data?.application?.walletStatus !== 'FROZEN') return null
const reviewStatus = terminalStatus === 'COMPLETED' ? 'PENDING_PROCESSING' : 'CANCELLED'
const review = await context.request('PUT', query(
`/api/platform/v1/last-leg/orders/${encodeURIComponent(id(created.body.data.application.lastlegOrderId))}/relabel-review`,
{ customerId: id(context.context.merchantQualificationFixture.tenantId) }), { status: reviewStatus, remark: context.marker })
assert(context, `${suffix}尾程订单审核直接贴标申请`, review,
success(review) && review.body.data?.application?.status === reviewStatus)
if (!success(review)) return null
if (terminalStatus === 'COMPLETED') {
const processing = await context.request('PUT', `${detailPath}/status`, { status: 'PROCESSING', remark: context.marker })
assert(context, '换标单冻结后才允许进入处理中', processing,
success(processing) && processing.body.data?.application?.status === 'PROCESSING')
const confirmPending = await context.request('PUT', `${detailPath}/status`, { status: 'CUSTOMER_CONFIRM_PENDING', remark: context.marker })
assert(context, '换标处理完成后进入客户确认', confirmPending,
success(confirmPending) && confirmPending.body.data?.application?.status === 'CUSTOMER_CONFIRM_PENDING')
const readyForOutbound = await context.request('PUT', `${detailPath}/status`, { status: 'READY_FOR_OUTBOUND', remark: context.marker })
assert(context, '换标客户确认后进入待出库', readyForOutbound,
success(readyForOutbound) && readyForOutbound.body.data?.application?.status === 'READY_FOR_OUTBOUND')
}
const terminal = terminalStatus === 'CANCELLED' ? review : await context.request('PUT', `${detailPath}/status`, { status: terminalStatus, remark: context.marker })
assert(context, `${suffix}换标单进入${terminalStatus}`, terminal,
success(terminal) && terminal.body.data?.application?.status === terminalStatus)
const walletTerminal = terminalStatus === 'COMPLETED' ? 'SETTLED' : 'RELEASED'
const wallet = await poll(async () => context.request('GET', detailPath),
(response) => success(response) && response.body.data?.application?.walletStatus === walletTerminal)
assert(context, `${suffix}换标费用跨区${walletTerminal}`, wallet.result,
wallet.result?.body?.data?.application?.walletStatus === walletTerminal,
`relabelNo=${relabelNo}, pollRequests=${wallet.attempts}, walletStatus=${wallet.result?.body?.data?.application?.walletStatus || 'missing'}`)
return { relabelNo, terminalStatus, walletStatus: walletTerminal, freezePollRequests: frozen.attempts, terminalPollRequests: wallet.attempts }
}
async function verifyClientWallet(context, token, fixture, wallet, procurement, relabels) {
const account = await context.request('GET', `${CLIENT_FINANCE}/account`, undefined, token)
const expectedActual = money(Number(wallet.before.actualBalance) + wallet.rechargeAmount - 0.02)
assert(context, '客户工作台余额与冻结结算结果一致', account,
success(account) && money(account.body.data?.actualBalance) === expectedActual
&& money(account.body.data?.frozenBalance) === money(wallet.before.frozenBalance)
&& money(account.body.data?.creditUsed) === money(wallet.before.creditUsed),
`expectedActual=${expectedActual}, actual=${account.body?.data?.actualBalance ?? 'missing'}, frozen=${account.body?.data?.frozenBalance ?? 'missing'}, creditUsed=${account.body?.data?.creditUsed ?? 'missing'}`)
const ledger = await context.request('GET', `${CLIENT_FINANCE}/account/ledger?page=1&size=100`, undefined, token)
const entries = rows(ledger?.body?.data)
const businessNos = [procurement.feeNo, ...relabels.map((item) => item.relabelNo)]
assert(context, '客户账本包含充值、冻结、结算和释放明细', ledger,
success(ledger) && entries.some((item) => item.changeType === 'RECHARGE')
&& businessNos.every((businessNo) => entries.some((item) => item.sourceNo === businessNo))
&& ['FREEZE', 'CONSUMPTION', 'UNFREEZE'].every((type) => entries.some((item) => item.changeType === type)),
`entries=${entries.length}, businessNos=${businessNos.join(',')}`)
return account?.body?.data
}
function summaryValid(value, fields) {
return fields.every((field) => Number.isInteger(Number(value?.[field])) && Number(value[field]) >= 0)
}
async function verifyPalletSummaries(context, warehouseId) {
const domestic = await context.request('GET', '/api/platform/v1/pallets/summary?tenantId=0&page=1&size=20')
const d = domestic?.body?.data
assert(context, '国内托盘汇总含闲置和占用数量', domestic,
success(domestic) && summaryValid(d, ['totalCount', 'idleCount', 'occupiedCount'])
&& Number(d.totalCount) === Number(d.idleCount) + Number(d.occupiedCount),
`total=${d?.totalCount}, idle=${d?.idleCount}, occupied=${d?.occupiedCount}`)
const overseas = await context.request('GET', '/api/platform/v1/overseas-warehouse-resources/pallets/summary?page=1&size=20')
const o = overseas?.body?.data
assert(context, '海外托盘汇总含闲置、占用和报废数量', overseas,
success(overseas) && summaryValid(o, ['totalCount', 'idleCount', 'occupiedCount', 'scrappedCount'])
&& Number(o.totalCount) === Number(o.idleCount) + Number(o.occupiedCount) + Number(o.scrappedCount),
`total=${o?.totalCount}, idle=${o?.idleCount}, occupied=${o?.occupiedCount}, scrapped=${o?.scrappedCount}`)
const warehouse = await context.request('GET', `/api/platform/v1/overseas-warehouse-resources/${encodeURIComponent(warehouseId)}/pallets/summary?page=1&size=20`)
assert(context, '海外仓级托盘汇总可用', warehouse,
success(warehouse) && summaryValid(warehouse.body.data, ['totalCount', 'idleCount', 'occupiedCount', 'scrappedCount']))
}
async function verifyDestuffCosts(context, completedFacts) {
const evidence = []
for (const fact of completedFacts) {
const businessNo = `DESTUFF-${fact.containerBookingId}`
const lastleg = await poll(async () => context.request('GET', query('/api/platform/v1/last-leg/finance-fee-sources', {
businessType: 'OVERSEAS_CONTAINER', businessNo,
})), (response) => success(response) && rows(response.body.data).length > 0, 75)
const sources = success(lastleg.result) ? rows(lastleg.result.body.data) : []
const allocated = money(sources.reduce((sum, item) => sum + Number(item.amount || 0), 0))
assert(context, '拆柜打托费用按客户体积分摊且金额守恒', lastleg.result,
sources.length > 0 && sources.every((item) => item.sourceType === 'OVERSEAS_CONTAINER_FEE'
&& item.feeItemCode === 'DESTUFF_PALLET' && Number(item.amount) > 0)
&& allocated === money(fact.expectedPalletCost),
`containerBookingId=${fact.containerBookingId}, sources=${sources.length}, allocated=${allocated}, expected=${money(fact.expectedPalletCost)}, pollRequests=${lastleg.attempts}`)
const finance = await poll(async () => context.request('GET', query(`${FINANCE}/fee-sources`, {
page: 1, pageSize: 200, keyword: businessNo,
})), (response) => success(response) && sources.length > 0
&& sources.every((source) => rows(response.body.data).some((item) => id(item.sourceFeeId) === id(source.sourceFeeId))), 60)
const delivered = success(finance.result) ? rows(finance.result.body.data)
.filter((item) => sources.some((source) => id(source.sourceFeeId) === id(item.sourceFeeId))) : []
assert(context, '拆柜打托费用跨区进入Finance且金额守恒', finance.result,
delivered.length === sources.length
&& money(delivered.reduce((sum, item) => sum + Number(item.amount || 0), 0)) === money(fact.expectedPalletCost),
`containerBookingId=${fact.containerBookingId}, delivered=${delivered.length}, expected=${sources.length}, pollRequests=${finance.attempts}`)
evidence.push({ containerBookingId: fact.containerBookingId, expectedPalletCost: money(fact.expectedPalletCost),
allocations: sources.length, lastlegPollRequests: lastleg.attempts, financePollRequests: finance.attempts })
}
return evidence
}
export async function run(context) {
const start = context.steps.length
const fixture = context.context?.merchantQualificationFixture
const completedFacts = context.createdResources?.phase4OverseasPda?.completedContainerFacts || []
const warehouseId = context.createdResources?.phase4OverseasPda?.overseasWarehouseId
|| context.context?.phase4OverseasWarehouse?.warehouseId
context.assert(MODULE, '钱包闭环必须使用本轮已审核客户和PDA实收柜', null,
Boolean(fixture?.tenantId && fixture?.username && fixture?.password && warehouseId && completedFacts.length > 0),
`fixture=${Boolean(fixture)}, warehouseId=${warehouseId || 'missing'}, completedContainers=${completedFacts.length}`, OPTIONS)
if (!fixture?.tenantId || !warehouseId || completedFacts.length === 0) return
const auth = await merchantLogin(context, fixture)
const wallet = auth ? await prepareWallet(context, id(fixture.tenantId)) : null
const procurement = wallet ? await procurementFlow(context, fixture, auth.accessToken) : null
const settledRelabel = procurement ? await relabelFlow(context, auth.accessToken, warehouseId, 'SETTLE', 'COMPLETED') : null
const releasedRelabel = settledRelabel ? await relabelFlow(context, auth.accessToken, warehouseId, 'RELEASE', 'CANCELLED') : null
const relabels = [settledRelabel, releasedRelabel].filter(Boolean)
const finalAccount = procurement && relabels.length === 2
? await verifyClientWallet(context, auth.accessToken, fixture, wallet, procurement, relabels) : null
await verifyPalletSummaries(context, warehouseId)
const destuffCosts = await verifyDestuffCosts(context, completedFacts)
context.createdResources.phase4OverseasWalletAndPalletCost = {
marker: context.marker, customerId: id(fixture.tenantId), procurement, relabels,
finalAccount, completedContainerCosts: destuffCosts,
}
const steps = context.steps.slice(start)
context.scenarios.push({ name: SCENARIO, module: MODULE, success: steps.every((step) => step.passed),
deploymentBlocked: steps.some((step) => step.deploymentBlocker && !step.passed),
passed: steps.filter((step) => step.passed).length, failed: steps.filter((step) => !step.passed).length })
}