const MODULE = 'finance-full-fixtures' const OPTIONS = { scenario: 'finance-full-fixture-production', deploymentBlocker: true } function id(value) { return value == null ? '' : String(value) } function key(context, operation) { return `${context.marker}-${operation}`.slice(0, 128) } function request(context, method, path, body, token = context.token, extra = {}) { return context.request(method, path, body, token, { 'Idempotency-Key': key(context, path.replaceAll('/', '_')), ...extra }) } function rows(value) { return Array.isArray(value) ? value : Array.isArray(value?.records) ? value.records : [] } function dateOffset(days) { const value = new Date(); value.setUTCDate(value.getUTCDate() + days); return value.toISOString().slice(0, 10) } function blocker(context, name, evidence) { context.addStep(MODULE, name, null, false, evidence, OPTIONS) } async function createOrder(context, suffix) { if (context.financeFixtureHooks?.createOrder) return context.financeFixtureHooks.createOrder(context, suffix) const originalMarker = context.marker const original = context.createdResources.orders context.marker = `${originalMarker}-${suffix}` try { const orders = await import(new URL('./orders.mjs', import.meta.url)) await orders.run(context) const created = context.createdResources.orders return created?.marker === context.marker && created?.orderId && created?.inboundId ? { ...created } : null } finally { context.marker = originalMarker if (original) context.createdResources.orders = original else delete context.createdResources.orders } } async function measureOrder(context, resource) { if (context.financeFixtureHooks?.measureOrder) return context.financeFixtureHooks.measureOrder(context, resource) const originalMarker = context.marker const createdKeys = ['orders', 'inboundMeasurement', 'ocrInbound', 'testInbound'] const contextKeys = ['inboundWeightOcr', 'ocrInbound', 'testInbound', 'createdInbound'] const createdSnapshot = new Map(createdKeys.map((key) => [key, { present: Object.hasOwn(context.createdResources, key), value: context.createdResources[key] }])) const contextSnapshot = new Map(contextKeys.map((key) => [key, { present: Object.hasOwn(context.context, key), value: context.context[key] }])) context.marker = resource.marker context.createdResources.orders = resource context.createdResources.ocrInbound = resource context.createdResources.testInbound = resource context.context.inboundWeightOcr = resource context.context.ocrInbound = resource context.context.testInbound = resource context.context.createdInbound = resource try { const measurement = await import(new URL('./inbound-measurement.mjs', import.meta.url)) await measurement.run(context) const result = context.createdResources.inboundMeasurement const detailResponse = await context.request('GET', `/api/platform/inbounds/${encodeURIComponent(resource.inboundId)}?tenantId=${encodeURIComponent(resource.tenantId)}`) const detail = context.apiSuccess(MODULE, '回读审核测量实重与总体积', detailResponse, `inboundId=${resource.inboundId}`, OPTIONS) const summary = detail?.summary || {} const actualWeightKg = Number(summary.actualGrossWeightKg ?? summary.actualWeightKg) const totalVolumeCbm = Number(summary.actualVolumeCbm ?? summary.totalVolumeCbm) context.assert(MODULE, '审核测量快照实重和总体积均大于零', detailResponse, Number.isFinite(actualWeightKg) && actualWeightKg > 0 && Number.isFinite(totalVolumeCbm) && totalVolumeCbm > 0, `actualWeightKg=${actualWeightKg || 'missing'}, totalVolumeCbm=${totalVolumeCbm || 'missing'}`, OPTIONS) return result?.marker === resource.marker ? { ...resource, ...result, actualWeightKg, totalVolumeCbm } : null } finally { context.marker = originalMarker for (const [key, snapshot] of createdSnapshot) { if (snapshot.present) context.createdResources[key] = snapshot.value else delete context.createdResources[key] } for (const [key, snapshot] of contextSnapshot) { if (snapshot.present) context.context[key] = snapshot.value else delete context.context[key] } } } async function loadFeeItem(context, direction) { if (context.financeFixtureHooks?.loadFeeItem) return context.financeFixtureHooks.loadFeeItem(context, direction) const response = await context.request('GET', '/api/platform/v1/fee-items?page=1&pageSize=200') const list = context.apiSuccess(MODULE, `查询${direction}人工费用项目`, response, '', OPTIONS) return rows(list).find((item) => item?.status === 'ACTIVE' && (direction === 'RECEIVABLE' ? item.warehouseReceivable === true : item.warehousePayable === true)) || null } async function createManualFee(context, resource, feeItem, direction, amount, basis, businessDate) { if (context.financeFixtureHooks?.createManualFee) return context.financeFixtureHooks.createManualFee(context, resource, feeItem, direction, amount, basis, businessDate) const path = `/api/platform/v1/inbounds/${encodeURIComponent(resource.inboundId)}/fee-details?accountId=${encodeURIComponent(resource.tenantId)}` const body = { feeItemId: Number(feeItem.id), direction, businessDate, unitPrice: amount, currency: 'CNY', amount, settlementIncluded: true, remark: `${context.marker}-${basis}` } const response = await request(context, 'POST', path, body) const detail = context.apiSuccess(MODULE, `${basis}人工费用明细`, response, `inboundId=${resource.inboundId}`, OPTIONS) const feeId = id(detail?.id) context.assert(MODULE, `${basis}费用来源身份和金额`, response, Boolean(feeId) && id(detail?.inboundId) === id(resource.inboundId) && Number(detail?.amount) === Number(amount), `feeId=${feeId || 'missing'}, amount=${detail?.amount ?? 'missing'}`, OPTIONS) if (!feeId) return null const approve = await request(context, 'POST', `${path}/${feeId}/approve`) context.apiSuccess(MODULE, `${basis}费用审核`, approve, '', OPTIONS) return { feeSourceId: feeId, sourceType: detail?.sourceType || 'MANUAL', direction, amount: Number(amount), businessDate, currency: 'CNY', billingBasis: basis, inboundId: id(resource.inboundId), orderId: id(resource.orderId), chargeableWeightKg: detail?.chargeableWeightKg ?? null, chargeableVolumeCbm: detail?.chargeableVolumeCbm ?? null } } async function calculateAutomaticFee(context, resource) { if (context.financeFixtureHooks?.calculateAutomaticFee) return context.financeFixtureHooks.calculateAutomaticFee(context, resource) const pricingInboundResponse = await context.request('GET', `/api/platform/inbounds/${encodeURIComponent(resource.inboundId)}?tenantId=${encodeURIComponent(resource.tenantId)}`) const pricingInbound = context.apiSuccess(MODULE, '计费前回读入库主表计费快照', pricingInboundResponse, `inboundId=${resource.inboundId}`, OPTIONS) const pricingSummary = pricingInbound?.summary || {} const actualWeightKg = Number(pricingSummary.actualGrossWeightKg ?? pricingSummary.actualWeightKg) const actualVolumeCbm = Number(pricingSummary.actualVolumeCbm ?? pricingSummary.totalVolumeCbm) if (!(actualWeightKg > 0) || !(actualVolumeCbm > 0)) { blocker(context, '自动计费前审核测量快照完整', `服务端计费实际读取 fl_inbound_order.actual_gross_weight_kg/actual_volume_cbm;inboundId=${resource.inboundId}, actualWeightKg=${actualWeightKg || 'missing'}, actualVolumeCbm=${actualVolumeCbm || 'missing'};测量表有值但主表未同步`) return null } const base = `/api/platform/v1/inbounds/${encodeURIComponent(resource.inboundId)}/fees` const query = `tenantId=${encodeURIComponent(resource.tenantId)}` const calculateResponse = await request(context, 'POST', `${base}/calculate?${query}`, {}) const calculated = context.apiSuccess(MODULE, '按审核测量快照执行真实自动计费', calculateResponse, `orderId=${resource.orderId}, inboundId=${resource.inboundId}`, OPTIONS) const feeSourceId = id(calculated?.feeId) const billingBasis = calculated?.chargeUnit === 'KG' ? 'WEIGHT_KG' : calculated?.chargeUnit === 'CBM' ? 'VOLUME_CBM' : null const responseWeightKg = Number(calculated?.actualWeightKg) const responseVolumeCbm = Number(calculated?.actualVolumeCbm) context.assert(MODULE, '自动费用返回来源、金额和计费依据', calculateResponse, Boolean(feeSourceId) && Boolean(billingBasis) && Number(calculated?.totalAmount) >= 0 && responseWeightKg > 0 && responseVolumeCbm > 0, `feeSourceId=${feeSourceId || 'missing'}, chargeUnit=${calculated?.chargeUnit || 'missing'}, amount=${calculated?.totalAmount ?? 'missing'}, actualWeightKg=${responseWeightKg || 'missing'}, actualVolumeCbm=${responseVolumeCbm || 'missing'}`, OPTIONS) if (!feeSourceId || !billingBasis) return null const confirmResponse = await request(context, 'POST', `${base}/confirm?${query}`) const confirmed = context.apiSuccess(MODULE, '确认真实自动费用快照', confirmResponse, '', OPTIONS) const sendResponse = await request(context, 'POST', `${base}/send?${query}`) const sent = context.apiSuccess(MODULE, '发布真实自动费用来源', sendResponse, '', OPTIONS) return { feeSourceId, orderId: id(resource.orderId), inboundId: id(resource.inboundId), sourceType: 'FBA_PRICING', direction: 'RECEIVABLE', currency: sent?.currency || confirmed?.currency || calculated.currency, amount: Number(sent?.totalAmount ?? confirmed?.totalAmount ?? calculated.totalAmount), businessDate: String(sent?.sentAt || confirmed?.confirmedAt || calculated.calculatedAt || '').slice(0, 10), billingBasis, chargeableQuantity: sent?.chargeableQuantity ?? calculated.chargeableQuantity, chargeUnit: sent?.chargeUnit || calculated.chargeUnit, actualWeightKg: responseWeightKg, actualVolumeCbm: responseVolumeCbm } } async function loadSupplier(context) { if (context.financeFixtureHooks?.loadSupplier) return context.financeFixtureHooks.loadSupplier(context) const response = await context.request('GET', '/api/platform/v1/partners?page=1&size=200&status=ACTIVE') const page = context.apiSuccess(MODULE, '查询真实供应商往来单位', response, '', OPTIONS) return rows(page).find((item) => item?.status === 'ACTIVE') || null } async function uploadVoucher(context, customerId) { if (context.financeFixtureHooks?.uploadVoucher) return context.financeFixtureHooks.uploadVoucher(context, customerId) const path = `/api/platform/finance/v1/vouchers?customerId=${encodeURIComponent(customerId)}` const form = new FormData() const png = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64') form.append('file', new Blob([png], { type: 'image/png' }), `finance-${context.marker}.png`) try { const response = await fetch(`${context.options.gateway}${path}`, { method: 'POST', headers: { Accept: 'application/json', Authorization: `Bearer ${context.token}` }, body: form, signal: AbortSignal.timeout(30_000) }) const body = await response.json().catch(() => null) const evidence = { method: 'POST', path, http: response.status, body, raw: null, requestId: body?.requestId || response.headers.get('x-request-id'), traceId: body?.traceId || response.headers.get('x-trace-id') } const data = context.apiSuccess(MODULE, '上传本轮 Finance 凭证', evidence, '', OPTIONS) return id(data?.fileId) || null } catch (error) { blocker(context, '上传本轮 Finance 凭证', `multipart request failed: ${String(error)}`) return null } } async function loadPricingEvidence(context) { if (context.financeFixtureHooks?.loadPricingEvidence) return context.financeFixtureHooks.loadPricingEvidence(context) const response = await context.request('GET', '/api/platform/v1/fba-pricing/carrier-rates/price-books?status=ACTIVE') const versions = context.apiSuccess(MODULE, '读取真实生效价格配置', response, '', OPTIONS) return { activeVersionIds: rows(versions).map((item) => id(item.id)).filter(Boolean) } } async function createContainerCost(context, customerId, container, voucherFileId) { if (context.financeFixtureHooks?.createContainerCost) return context.financeFixtureHooks.createContainerCost(context, customerId, container, voucherFileId) const body = { customerId: Number(customerId), costType: 'CONTAINER_SHARED', orderId: null, containerId: Number(container.containerBookingId), expenseName: `${context.marker}-整柜成本`, amount: 300, currency: 'CNY', voucherFileIds: [voucherFileId], remark: context.marker } const response = await request(context, 'POST', '/api/platform/finance/v1/cost-entries', body) const cost = context.apiSuccess(MODULE, '创建整柜成本并触发真实体积分摊', response, '', OPTIONS) return cost?.id ? { costId: id(cost.id), allocations: Array.isArray(cost.allocations) ? cost.allocations : null } : null } export async function run(context) { const start = context.steps.length const pricingEvidence = await loadPricingEvidence(context) const resources = [] for (const suffix of ['AUTO1', 'AUTO2', 'AUTO3', 'AUTO4', 'MANUAL']) { const resource = await createOrder(context, suffix) context.assert(MODULE, `创建 ${suffix} marker 隔离订单与入库单`, null, Boolean(resource), resource ? `orderId=${resource.orderId}, inboundId=${resource.inboundId}` : `marker=${context.marker}-${suffix} 未返回独立入库单`, OPTIONS) if (resource) resources.push(resource) } const measured = [] for (const resource of resources) { const result = await measureOrder(context, resource) if (result) measured.push({ ...resource, ...result }) } const feeItems = { RECEIVABLE: await loadFeeItem(context, 'RECEIVABLE'), PAYABLE: await loadFeeItem(context, 'PAYABLE'), } const supplier = await loadSupplier(context) if (!supplier?.id) blocker(context, '取得真实 PAYABLE 往来单位', '未找到 ACTIVE 往来单位;不能伪造 supplierId/partnerId') for (const direction of ['RECEIVABLE', 'PAYABLE']) { if (!feeItems[direction]) context.addCoverageGap(MODULE, `missing-${direction.toLowerCase()}-fee-item`, `${direction} 人工费用 fixture`, `没有找到带 warehouse${direction === 'RECEIVABLE' ? 'Receivable' : 'Payable'}=true 的 ACTIVE 费用项目,禁止猜测费用项目 ID`, 'missing-deployed-fee-item') } const fixtures = [] for (const resource of measured.slice(0, 4)) { const fee = await calculateAutomaticFee(context, resource) if (fee) fixtures.push(fee) } for (const basis of ['WEIGHT_KG', 'VOLUME_CBM']) { if (!fixtures.some((fee) => fee.billingBasis === basis)) { blocker(context, `${basis} 自动计费 fixture`, `activePriceVersions=${pricingEvidence.activeVersionIds?.join(',') || 'none'};本轮隔离订单未产生 ${basis} 来源,价格配置/订单条件不支持,不能伪装成功`) } } const manualResource = measured.at(-1) if (manualResource && feeItems.PAYABLE && supplier?.id) { const fee = await createManualFee(context, manualResource, feeItems.PAYABLE, 'PAYABLE', 80, 'MANUAL_ORDER_FEE', dateOffset(-1)) if (fee) fixtures.push({ ...fee, supplierId: id(supplier.id), partnerId: id(supplier.id), supplierName: supplier.partnerName || null }) } const container = context.createdResources?.phase3ContainerLoading const containerOwned = String(container?.marker || '').startsWith(`${context.marker}-`) && container?.loadingPlanId && container?.containerBookingId let containerFixture = null if (containerOwned && feeItems.PAYABLE && supplier?.id) { const path = `/api/platform/v1/loading-plans/${encodeURIComponent(container.loadingPlanId)}/fee-details?accountId=${encodeURIComponent(container.tenantId)}` const body = { feeItemId: Number(feeItems.PAYABLE.id), direction: 'PAYABLE', businessDate: dateOffset(-2), amount: 300, currency: 'CNY', settlementIncluded: true, remark: `${context.marker}-CONTAINER_SHARED` } const response = await request(context, 'POST', path, body) const detail = context.apiSuccess(MODULE, '创建按柜费用来源', response, '', OPTIONS) const feeId = id(detail?.id) if (feeId) { const approval = await request(context, 'POST', `${path}/${feeId}/approve`); context.apiSuccess(MODULE, '审核按柜费用来源', approval, '', OPTIONS) } containerFixture = { feeSourceId: feeId || null, sourceType: 'MANUAL', direction: 'PAYABLE', amount: 300, businessDate: body.businessDate, currency: 'CNY', billingBasis: 'CONTAINER_SHARED', supplierId: supplier?.id ? id(supplier.id) : null, partnerId: supplier?.id ? id(supplier.id) : null, containerBookingId: id(container.containerBookingId), containerNo: container.containerNo || null, loadingPlanId: id(container.loadingPlanId) } } else { context.addStep(MODULE, '取得同 marker 柜次用于整柜费用分摊', null, false, '没有同 marker 的已完成柜次和可核验 containerBookingId;不读取历史柜次、不猜测柜号', OPTIONS) context.addCoverageGap(MODULE, 'container-shared-cost-allocation', '按柜添加费用并按订单/运单体积分摊', '当前上下文没有同 marker 的已完成柜次及真实 Firstleg 体积快照,禁止借用历史柜次或模拟 Finance 分摊成功', 'no-marker-owned-container-snapshot') } if (containerFixture) fixtures.push(containerFixture) const customerId = id(measured[0]?.tenantId) || null const voucherFileId = customerId ? await uploadVoucher(context, customerId) : null if (!voucherFileId) blocker(context, '取得本轮 Finance voucherFileId', '未通过 /api/platform/finance/v1/vouchers multipart 上传取得真实 fileId;整柜成本不得模拟成功') let allocations = [] if (containerFixture?.feeSourceId && voucherFileId && supplier?.id) { const cost = await createContainerCost(context, customerId, container, voucherFileId) if (Array.isArray(cost?.allocations)) allocations = cost.allocations else blocker(context, '回读整柜成本分摊结果', `costId=${cost?.costId || 'missing'};Finance cost-entries 创建响应未返回逐订单/运单 allocations,禁止从金额和体积自行伪造持久化结果`) } if (allocations.length === 0) blocker(context, '输出真实订单/运单分摊', '缺少已完成柜次、凭证、供应商或 Finance 分摊响应;不伪造 allocation ID/金额') const distinctBusinessDates = new Set(fixtures.map((item) => item.businessDate).filter(Boolean)) if (distinctBusinessDates.size < 2) blocker(context, '费用来源覆盖不同业务日期', `businessDates=${[...distinctBusinessDates].join(',') || 'none'};至少需要两个真实日期以验收按日期排除`) if (measured.length > 0 && measured.every((item) => !item.waybillId)) blocker(context, '取得订单真实 waybillId', '当前订单/入库接口未返回独立 waybillId;输出 null,不使用 orderId、orderNo 或 inboundId 冒充运单 ID') context.createdResources.financeFullFixtures = { marker: context.marker, customerId, orders: measured.map((item) => ({ orderId: id(item.orderId), orderNo: item.orderNo || null, waybillId: item.waybillId ? id(item.waybillId) : null, inboundId: id(item.inboundId), actualWeightKg: item.actualWeightKg, totalVolumeCbm: item.totalVolumeCbm, businessDate: fixtures.find((fee) => fee.orderId === id(item.orderId))?.businessDate || null })), feeSources: fixtures, container: containerFixture ? { containerBookingId: containerFixture.containerBookingId, containerNo: containerFixture.containerNo } : { containerBookingId: null, containerNo: null }, allocations, supplier: supplier?.id ? { supplierId: id(supplier.id), partnerId: id(supplier.id), supplierName: supplier.partnerName || null } : null, voucherFileId, pricingEvidence, deploymentBlocker: context.steps.slice(start).filter((step) => step.deploymentBlocker).map((step) => step.evidence), } const steps = context.steps.slice(start) context.scenarios.push({ name: OPTIONS.scenario, module: MODULE, success: steps.every((step) => step.passed), deploymentBlocked: steps.some((step) => step.deploymentBlocker), passed: steps.filter((step) => step.passed).length, failed: steps.filter((step) => !step.passed).length }) }