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) } export function normalizeFinanceFixtureSources(sources, orders, allocations, customerId, marker) { const orderById = new Map(rows(orders).map((order) => [id(order?.orderId ?? order?.id), order])) const allocationOrders = rows(allocations).map((allocation) => orderById.get(id(allocation?.orderId))).filter(Boolean) return rows(sources).map((source) => { const sourceOrder = orderById.get(id(source?.orderId)) || (source?.billingBasis === 'CONTAINER_SHARED' ? allocationOrders[0] : null) return { ...source, customerId: id(source?.customerId ?? customerId) || null, marker: source?.marker ?? marker, orderId: id(source?.orderId ?? sourceOrder?.orderId ?? sourceOrder?.id) || null, waybillId: id(source?.waybillId ?? sourceOrder?.waybillId) || null, shipmentId: id(source?.shipmentId ?? sourceOrder?.shipmentId) || null, orderNo: source?.orderNo ?? sourceOrder?.orderNo ?? null, } }) } export function supplierCategoryForFeeItem(item) { return ['BOOKING', 'CUSTOMS', 'TRUCKING', 'CERTIFICATE'].includes(item?.supplierCategory) ? item.supplierCategory : null } export function buildSupplierBody(marker, category) { const suffix = String(marker || '').replace(/[^0-9A-Za-z]/g, '').slice(-24) return { name: `财务验收供应商-${suffix}`, category, truckingScope: category === 'TRUCKING' ? 'DOMESTIC' : null, ...(category === 'BOOKING' ? { carrierCodes: ['MATSON'] } : {}), region: '宁波', legalRepresentative: '财务验收法人', contactName: '财务验收联系人', phone: '13800000000', email: `finance-supplier-${suffix || 'fixture'}@example.com`, address: '财务验收地址', businessLicenseNo: `FIN${suffix}`.slice(0, 64), licenseFileId: null, status: 'ACTIVE', settlementPeriod: 'MONTHLY_30', remark: marker, } } async function createOrder(context, suffix, pricingProfile = null) { if (context.financeFixtureHooks?.createOrder) return context.financeFixtureHooks.createOrder(context, suffix, pricingProfile) const originalMarker = context.marker const original = context.createdResources.orders const originalFetch = globalThis.fetch context.marker = `${originalMarker}-${suffix}` if (pricingProfile && typeof originalFetch === 'function') { // The orders module discovers candidates independently. Reorder only those // real responses so its payload uses one exact active price-book row. globalThis.fetch = async (input, init = {}) => { const url = String(input) let effectiveInit = init if ((init.method || 'GET') === 'POST' && url.includes('/api/merchant/v1/first-leg/pricing/estimate') && typeof init.body === 'string') { const body = JSON.parse(init.body) effectiveInit = { ...init, body: JSON.stringify({ ...body, chargeMode: pricingProfile.chargeUnit }) } } const response = await originalFetch(input, effectiveInit) if ((init.method || 'GET') === 'GET' && (url.includes('/api/merchant/v1/warehouses?') || url.includes('/api/merchant/v1/first-leg/pricing/channels?'))) { const body = await response.clone().json().catch(() => null) const data = body?.code === '200' ? body.data : null if (url.includes('warehouseType=FBA')) { const records = Array.isArray(data?.records) ? data.records : [] const match = records.find((item) => id(item.warehouseCode || item.code) === id(pricingProfile.destinationCode)) if (match) body.data = { ...data, records: [match, ...records.filter((item) => item !== match)] } } else if (url.includes('warehouseType=DOMESTIC')) { const records = Array.isArray(data?.records) ? data.records : [] const match = records.find((item) => id(item.id) === id(pricingProfile.originWarehouseId)) if (match) body.data = { ...data, records: [match, ...records.filter((item) => item !== match)] } } else { const channels = Array.isArray(data) ? data : [] const match = channels.find((item) => id(item.channelCode) === id(pricingProfile.channelCode)) if (match) body.data = [{ ...match, pricingTaxModes: [pricingProfile.pricingTaxMode, ...(match.pricingTaxModes || []).filter((mode) => mode !== pricingProfile.pricingTaxMode)] }, ...channels.filter((item) => item !== match)] } return new Response(JSON.stringify(body), { status: response.status, statusText: response.statusText, headers: response.headers }) } return response } } 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, pricingProfile } : null } finally { globalThis.fetch = originalFetch 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', 'allowAnyExistingPallet', 'waitSplitVariant'] 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 // Finance pricing profiles can point at any configured domestic warehouse. // Reuse that warehouse's real pallet master instead of requiring the bulk-data // generator's two fixed Yiwu pallet codes. context.context.allowAnyExistingPallet = true context.context.waitSplitVariant = { ...(context.context.waitSplitVariant || {}), // The production pricing service chooses the dominant real billing basis. // Keep the CBM sample's measured gross weight below its measured volume // weight so the fixture proves the actual VOLUME_CBM path. grossWeightPerPalletKg: resource.pricingProfile?.chargeUnit === 'CBM' ? 20 : 100, } 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, supplier = null) { if (context.financeFixtureHooks?.createManualFee) return context.financeFixtureHooks.createManualFee(context, resource, feeItem, direction, amount, basis, businessDate) const basePath = `/api/platform/v1/inbounds/${encodeURIComponent(resource.inboundId)}/fee-details` const listPath = `${basePath}?accountId=${encodeURIComponent(resource.tenantId)}` const body = { feeItemId: String(feeItem.id), direction, businessDate, unitPrice: amount, currency: 'CNY', amount, ...(direction === 'PAYABLE' ? { settlementMode: 'ON_ACCOUNT' } : {}), supplierId: supplier?.id ? String(supplier.id) : null, remark: `${context.marker}-${basis}` } const response = await request(context, 'POST', listPath, 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', `${basePath}/${feeId}/approve?accountId=${encodeURIComponent(resource.tenantId)}`) 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 loadPublishedAutomaticFee(context, resource) { if (context.financeFixtureHooks?.loadPublishedAutomaticFee) return context.financeFixtureHooks.loadPublishedAutomaticFee(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 pricingBase = `/api/platform/v1/inbounds/${encodeURIComponent(resource.inboundId)}/fees` const tenantQuery = `tenantId=${encodeURIComponent(resource.tenantId)}` const latestResponse = await context.request('GET', `${pricingBase}/latest?${tenantQuery}`) let latest = context.apiSuccess(MODULE, '回读测量审核生成的自动费用试算', latestResponse, `inboundId=${resource.inboundId}`, OPTIONS) if (!latest?.feeId || !['CALCULATED', 'CONFIRMED', 'SENT'].includes(latest?.feeStatus)) { blocker(context, '测量审核已生成可确认的自动费用试算', `inboundId=${resource.inboundId}, feeId=${id(latest?.feeId) || 'missing'}, feeStatus=${latest?.feeStatus || 'missing'}, requestId=${latestResponse?.requestId || 'missing'}, traceId=${latestResponse?.traceId || 'missing'};审核只负责初算,Finance fixture 不重复试算`) return null } if (latest.feeStatus === 'CALCULATED') { const confirmResponse = await request(context, 'POST', `${pricingBase}/confirm?${tenantQuery}`) latest = context.apiSuccess(MODULE, '确认测量审核生成的自动费用试算', confirmResponse, `inboundId=${resource.inboundId}, feeId=${id(latest.feeId)}`, OPTIONS) } if (latest?.feeStatus === 'CONFIRMED') { const sendResponse = await request(context, 'POST', `${pricingBase}/send?${tenantQuery}`) latest = context.apiSuccess(MODULE, '发送已确认自动费用并发布来源', sendResponse, `inboundId=${resource.inboundId}, feeId=${id(latest.feeId)}`, OPTIONS) } if (latest?.feeStatus !== 'SENT') { blocker(context, '自动费用确认发送状态闭环', `inboundId=${resource.inboundId}, feeId=${id(latest?.feeId) || 'missing'}, feeStatus=${latest?.feeStatus || 'missing'}`) return null } const listPath = `/api/platform/v1/inbounds/${encodeURIComponent(resource.inboundId)}/fee-details?accountId=${encodeURIComponent(resource.tenantId)}` const listResponse = await context.request('GET', listPath) const details = context.apiSuccess(MODULE, '回读审核链已发布自动费用来源', listResponse, `inboundId=${resource.inboundId}`, OPTIONS) const published = rows(details).find((item) => item?.sourceType === 'FBA_PRICING' && item?.reviewStatus === 'APPROVED' && item?.publicationStatus === 'PUBLISHED') if (!published) { blocker(context, '审核链已生成可结算自动费用来源', `inboundId=${resource.inboundId}, processStatus=${pricingSummary.processStatus || pricingInbound?.processStatus || 'missing'}, feeId=${id(latest.feeId)}, feeStatus=${latest.feeStatus}, feeDetails=${rows(details).length};确认和发送成功后应同步生成 APPROVED + PUBLISHED 的 FBA_PRICING 来源`) return null } const feeSourceId = id(published.sourceFeeId) const billingBasis = published.chargeUnit === 'KG' ? 'WEIGHT_KG' : published.chargeUnit === 'CBM' ? 'VOLUME_CBM' : null const responseWeightKg = Number(published.actualGrossWeightKg ?? actualWeightKg) const responseVolumeCbm = Number(published.actualVolumeCbm ?? actualVolumeCbm) context.assert(MODULE, '已发布自动费用来源、金额和计费依据完整', listResponse, Boolean(feeSourceId) && Boolean(billingBasis) && Number(published.amount) >= 0 && responseWeightKg > 0 && responseVolumeCbm > 0, `feeSourceId=${feeSourceId || 'missing'}, detailId=${id(published.id) || 'missing'}, chargeUnit=${published.chargeUnit || 'missing'}, amount=${published.amount ?? 'missing'}, actualWeightKg=${responseWeightKg || 'missing'}, actualVolumeCbm=${responseVolumeCbm || 'missing'}`, OPTIONS) if (!feeSourceId || !billingBasis) return null return { feeSourceId, orderId: id(resource.orderId), inboundId: id(resource.inboundId), sourceType: 'FBA_PRICING', direction: 'RECEIVABLE', currency: published.currency, amount: Number(published.amount), businessDate: String(published.businessDate || published.publishedAt || '').slice(0, 10), billingBasis, chargeableQuantity: published.chargeableQuantity, chargeUnit: published.chargeUnit, actualWeightKg: responseWeightKg, actualVolumeCbm: responseVolumeCbm } } export async function loadSupplier(context, category, { carrierCode, truckingScope } = {}) { if (context.financeFixtureHooks?.loadSupplier) return context.financeFixtureHooks.loadSupplier(context, category) if (context.options?.fixedCustomerId) { const matches = item => id(item?.id) && item.status === 'ACTIVE' && (!category || item.category === category) && (!carrierCode || item.carrierCodes?.includes(carrierCode)) && (!truckingScope || item.truckingScope === truckingScope) for (let page = 1; ; page += 1) { const response = await request(context, 'GET', `/api/platform/v1/suppliers?tenantId=0&status=ACTIVE&page=${page}&size=100`) const data = context.apiSuccess(MODULE, '读取可复用的现有供应商', response, '', OPTIONS) if (!data) return null const records = rows(data) const candidate = records.find(matches) if (candidate) { const detailResponse = await request(context, 'GET', `/api/platform/v1/suppliers/${encodeURIComponent(candidate.id)}?tenantId=0`) const detail = context.apiSuccess(MODULE, '核对复用供应商状态与类别', detailResponse, '', OPTIONS) if (id(detail?.id) === id(candidate.id) && matches(detail)) return detail blocker(context, '复用供应商已变化', '候选详情不匹配,不创建替代供应商') return null } if (!records.length || page * Number(data.size || 100) >= Number(data.total ?? records.length)) break } blocker(context, '缺少可复用供应商', `需要 ACTIVE ${category || '任意类别'} 供应商;禁止自动创建`) return null } const effectiveCategory = category || 'BOOKING' const body = buildSupplierBody(context.marker, effectiveCategory) const response = await request(context, 'POST', '/api/platform/v1/suppliers?tenantId=0', body) const supplierId = id(context.apiSuccess(MODULE, '创建本轮 Finance 隔离供应商', response, `category=${effectiveCategory}`, OPTIONS)) if (!supplierId) return null const detailResponse = await context.request('GET', `/api/platform/v1/suppliers/${encodeURIComponent(supplierId)}?tenantId=0`) const detail = context.apiSuccess(MODULE, '回读本轮 Finance 隔离供应商', detailResponse, `supplierId=${supplierId}`, OPTIONS) context.assert(MODULE, 'Finance 供应商为 ACTIVE 且类别匹配费用项目', detailResponse, id(detail?.id) === supplierId && detail?.status === 'ACTIVE' && (!category || detail?.category === category) && detail?.name === body.name, `supplierId=${supplierId}, status=${detail?.status || 'missing'}, category=${detail?.category || 'missing'}`, OPTIONS) return detail?.status === 'ACTIVE' && (!category || detail?.category === category) ? detail : 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 fbaInternalCodes = new Set() let warehousePage = 1 let warehouseTotal = 0 do { const warehouseResponse = await context.request('GET', `/api/platform/v1/warehouse-admin?tenantId=0&page=${warehousePage}&size=200&warehouseType=FBA&status=ACTIVE`) const warehouseResult = context.apiSuccess(MODULE, `读取具备内部编号的 FBA 仓第 ${warehousePage} 页`, warehouseResponse, '', OPTIONS) const warehouses = rows(warehouseResult) warehouseTotal = Number(warehouseResult?.total || warehouses.length) warehouses.filter((item) => item?.internalCode && item?.warehouseCode) .forEach((item) => fbaInternalCodes.add(String(item.warehouseCode).trim().toUpperCase())) if (warehouses.length === 0) break warehousePage += 1 } while ((warehousePage - 1) * 200 < warehouseTotal) context.assert(MODULE, '自动计费候选使用已配置内部编号的 FBA 仓', null, fbaInternalCodes.size > 0, `warehouses=${fbaInternalCodes.size}`, OPTIONS) const response = await context.request('GET', '/api/platform/v1/fba-pricing/carrier-rates/price-books?status=ACTIVE') const versions = context.apiSuccess(MODULE, '读取真实生效价格配置', response, '', OPTIONS) const active = rows(versions).filter((item) => item?.status === 'ACTIVE') const profiles = [] for (const version of active) { const detailResponse = await context.request('GET', `/api/platform/v1/fba-pricing/carrier-rates/price-books/${encodeURIComponent(version.id)}`) const detail = context.apiSuccess(MODULE, '读取 ACTIVE 价格簿真实费率行', detailResponse, `versionId=${version.id}`, OPTIONS) const rates = Array.isArray(detail?.rates) ? detail.rates : [] for (const rate of rates) { if (rate?.destinationType !== 'FBA' || !rate?.originWarehouseId || !rate?.channelCode || !rate?.pricingTaxMode || !rate?.destinationCode) continue if (!fbaInternalCodes.has(String(rate.destinationCode).trim().toUpperCase())) continue if (!['KG', 'CBM'].includes(rate.chargeUnit) || profiles.some((item) => item.chargeUnit === rate.chargeUnit)) continue const matches = rates.filter((item) => id(item.originWarehouseId) === id(rate.originWarehouseId) && item.channelCode === rate.channelCode && item.destinationType === rate.destinationType && String(item.destinationCode).toUpperCase() === String(rate.destinationCode).toUpperCase() && item.pricingTaxMode === rate.pricingTaxMode) if (matches.length !== 1) continue profiles.push({ versionId: id(version.id), rateId: id(rate.id), chargeUnit: rate.chargeUnit, originWarehouseId: id(rate.originWarehouseId), channelCode: rate.channelCode, pricingTaxMode: rate.pricingTaxMode, destinationCode: rate.destinationCode, businessType: rate.businessType || 'FBA' }) } } context.assert(MODULE, 'ACTIVE 价格簿包含 KG 与 CBM 可用真实组合', response, profiles.some((item) => item.chargeUnit === 'KG') && profiles.some((item) => item.chargeUnit === 'CBM'), `units=${profiles.map((item) => item.chargeUnit).join(',') || 'none'}`, OPTIONS) return { activeVersionIds: active.map((item) => id(item.id)).filter(Boolean), profiles } } export async function ensureCustomerChargeRule(context, resource, profile) { if (context.financeFixtureHooks?.ensureCustomerChargeRule) { return context.financeFixtureHooks.ensureCustomerChargeRule(context, resource, profile) } if (!resource?.tenantId || profile?.pricingTaxMode !== 'TAX_EXCLUDED') return null const tenantId = id(resource.tenantId) const inboundResponse = await context.request('GET', `/api/platform/inbounds/${encodeURIComponent(resource.inboundId)}?tenantId=${encodeURIComponent(tenantId)}`) const inbound = context.apiSuccess(MODULE, '读取不包税入库计费业务类型', inboundResponse, `inboundId=${resource.inboundId}`, OPTIONS) const businessType = inbound?.summary?.businessType || profile.businessType if (!businessType) { blocker(context, '取得不包税入库计费业务类型', `inboundId=${resource.inboundId} 未返回 businessType,不能创建猜测的客户计费规则`) return null } const listPath = `/api/platform/v1/fba-pricing/customer-rules?tenantId=${encodeURIComponent(tenantId)}&customerId=${encodeURIComponent(tenantId)}` const listResponse = await context.request('GET', listPath) const listed = context.apiSuccess(MODULE, '查询不包税客户计费规则', listResponse, `customerId=${tenantId}, businessType=${businessType}`, OPTIONS) const applicable = rows(listed).filter((rule) => rule?.status === 'ACTIVE' && rule?.businessType === businessType && rule?.taxMode === 'TAX_EXCLUDED') let desired = applicable.find((rule) => rule?.chargeMode === profile.chargeUnit) if (context.options?.fixedCustomerId) { const defaults = applicable.filter(rule => rule.defaultRule === true) const selected = applicable.length === 1 ? applicable[0] : defaults.length === 1 ? defaults[0] : null const valid = selected?.id && id(selected.customerId) === tenantId && selected.chargeMode === profile.chargeUnit context.assert(MODULE, '固定客户复用现有有效默认计费方式', listResponse, Boolean(valid), `customerId=${tenantId}, chargeMode=${profile.chargeUnit}`, OPTIONS) if (!valid) throw new Error('固定客户缺少匹配且无歧义的计费规则,禁止自动创建或切换默认规则') return selected } if (!desired) { const effectiveFrom = new Date(Date.now() - 86_400_000).toISOString().slice(0, 19) const body = { ruleCode: `FIN_P1_${profile.chargeUnit}_${tenantId}`.slice(0, 50), customerId: tenantId, customerName: String(inbound?.summary?.customerName || `财务P1验收客户-${tenantId}`).slice(0, 128), businessType, chargeMode: profile.chargeUnit, taxMode: 'TAX_EXCLUDED', conversionFactor: 1, roundingScale: 3, roundingMode: 'HALF_UP', currency: 'CNY', effectiveFrom, effectiveTo: null, remark: `${context.marker}-VOLUME_CBM` } const createResponse = await request(context, 'POST', `/api/platform/v1/fba-pricing/customer-rules/versions?tenantId=${encodeURIComponent(tenantId)}`, body) desired = context.apiSuccess(MODULE, '创建不包税客户计费规则', createResponse, `customerId=${tenantId}, chargeMode=${profile.chargeUnit}`, OPTIONS) } const needsDefault = applicable.length > 1 || (applicable.length === 1 && id(applicable[0]?.id) !== id(desired?.id)) if (desired?.id && needsDefault && desired.defaultRule !== true) { const defaultResponse = await request(context, 'PUT', `/api/platform/v1/fba-pricing/customer-rules/${encodeURIComponent(desired.id)}/default?tenantId=${encodeURIComponent(tenantId)}`) context.apiSuccess(MODULE, '设置不包税客户唯一默认计费规则', defaultResponse, `ruleId=${desired.id}, chargeMode=${profile.chargeUnit}`, OPTIONS) } context.assert(MODULE, '不包税自动计费具备客户计费方式', listResponse, Boolean(desired?.id) && desired?.chargeMode === profile.chargeUnit, `customerId=${tenantId}, chargeMode=${desired?.chargeMode || 'missing'}, ruleId=${id(desired?.id) || 'missing'}`, OPTIONS) return desired || null } async function createMarkerContainer(context, resource) { if (context.financeFixtureHooks?.createMarkerContainer) return context.financeFixtureHooks.createMarkerContainer(context, resource) if (!resource) { blocker(context, '取得可创建柜次的测量入库单', '本轮没有完成测量的 marker-owned 入库单;跳过柜次及整柜成本 fixture,等待订单/测量前置修复') return null } if (context.financeFixtureHooks) return null const original = context.createdResources.phase3ContainerLoading const originalMarker = context.marker const snapshots = ['orders', 'inboundMeasurement'].map((key) => [key, { present: Object.hasOwn(context.createdResources, key), value: context.createdResources[key] }]) context.createdResources.orders = resource context.createdResources.inboundMeasurement = resource context.marker = resource.marker || originalMarker try { const module = await import(new URL('./phase3-container-loading.mjs', import.meta.url)) await module.run(context) const created = context.createdResources.phase3ContainerLoading return created?.marker === context.marker && created?.containerBookingId && created?.loadingPlanId ? { ...created } : null } finally { context.marker = originalMarker for (const [key, snapshot] of snapshots) { if (snapshot.present) context.createdResources[key] = snapshot.value else delete context.createdResources[key] } if (original) context.createdResources.phase3ContainerLoading = original else delete context.createdResources.phase3ContainerLoading } } export function buildContainerCostBody(marker, customerId, container, voucherFileId) { return { customerId: String(customerId), costType: 'CONTAINER_SHARED', orderId: null, // Finance names this field containerId, but its Firstleg cost snapshot contract // resolves it as the loading plan ID, not the physical container booking ID. containerId: String(container.loadingPlanId), expenseName: `${marker}-整柜成本`, amount: 300, currency: 'CNY', voucherFileIds: [voucherFileId], remark: marker } } async function createContainerCost(context, customerId, container, voucherFileId, snapshotInput) { if (context.financeFixtureHooks?.createContainerCost) return context.financeFixtureHooks.createContainerCost(context, customerId, container, voucherFileId, snapshotInput) const body = buildContainerCostBody(context.marker, customerId, container, voucherFileId) const response = await request(context, 'POST', '/api/platform/finance/v1/cost-entries', body) if (response?.body?.code === 'FIN_400') { const first = snapshotInput?.items?.[0] blocker(context, 'Finance 整柜成本请求关联真实装柜计划', `message=${response.body.message || 'missing'}, loadingPlanId=${container.loadingPlanId || 'missing'}, containerBookingId=${container.containerBookingId || 'missing'}, voucherFileId=${voucherFileId || 'missing'}, publicSnapshotValid=${snapshotInput?.valid === true}, validItems=${snapshotInput?.items?.length || 0}, accountId=${first?.accountId ?? 'missing'}, orderId=${first?.orderId ?? 'missing'}, volumeCbm=${first?.volumeCbm ?? 'missing'}, requestId=${response.requestId || 'missing'}, traceId=${response.traceId || 'missing'};若 publicSnapshotValid=true,说明 fixture ID 与公开装柜明细有效,阻断位于 Finance -> Firstleg 内部快照服务部署/契约`) } const cost = context.apiSuccess(MODULE, '创建整柜成本并触发真实体积分摊', response, '', OPTIONS) if (!cost?.id) return null const detailResponse = await context.request('GET', `/api/platform/finance/v1/cost-entries/${encodeURIComponent(cost.id)}?customerId=${encodeURIComponent(customerId)}`) const detail = context.apiSuccess(MODULE, '回读整柜成本分摊结果', detailResponse, `costId=${cost.id}`, OPTIONS) return { costId: id(cost.id), allocations: Array.isArray(detail?.allocations) ? detail.allocations : null } } async function verifyContainerCostSnapshotInput(context, customerId, container) { if (context.financeFixtureHooks?.verifyContainerCostSnapshotInput) { return context.financeFixtureHooks.verifyContainerCostSnapshotInput(context, customerId, container) } const path = `/api/platform/v1/first-leg/smart-loading/plans/${encodeURIComponent(container.loadingPlanId)}` const response = await context.request('GET', path) const plan = context.apiSuccess(MODULE, '回读 Finance 柜次体积快照输入', response, `loadingPlanId=${container.loadingPlanId}`, OPTIONS) const items = Array.isArray(plan?.items) ? plan.items : [] const valid = items.filter((item) => id(item?.accountId) === id(customerId) && Number(item?.orderId) > 0 && Number(item?.volumeCbm) > 0) context.assert(MODULE, '装柜明细满足 Finance 内部快照筛选条件', response, plan?.status === 'COMPLETED' && valid.length > 0, `status=${plan?.status || 'missing'}, items=${items.length}, validItems=${valid.length}, accountId=${items[0]?.accountId ?? 'missing'}, orderId=${items[0]?.orderId ?? 'missing'}, volumeCbm=${items[0]?.volumeCbm ?? 'missing'}`, OPTIONS) return { valid: plan?.status === 'COMPLETED' && valid.length > 0, items: valid } } export async function run(context) { const start = context.steps.length const pricingEvidence = await loadPricingEvidence(context) const resources = [] const profiles = pricingEvidence.profiles || [] for (const [index, suffix] of ['AUTO1', 'AUTO2', 'AUTO3', 'AUTO4', 'MANUAL', 'CONTAINER'].entries()) { const resource = await createOrder(context, suffix, profiles[index % profiles.length] || null) 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) } for (const resource of resources.filter((item) => item.pricingProfile?.pricingTaxMode === 'TAX_EXCLUDED')) { await ensureCustomerChargeRule(context, resource, resource.pricingProfile) } 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 payableSupplierCategory = supplierCategoryForFeeItem(feeItems.PAYABLE) const supplier = await loadSupplier(context, payableSupplierCategory) if (!supplier?.id) blocker(context, '取得 Warehouse PAYABLE 供应商', `未找到 ACTIVE ${payableSupplierCategory || '任意类别'} 供应商;不能伪造 supplierId`) 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 loadPublishedAutomaticFee(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 receivableResources = measured.slice(0, 4) if (feeItems.RECEIVABLE) { for (const [index, resource] of receivableResources.entries()) { const fee = await createManualFee(context, resource, feeItems.RECEIVABLE, 'RECEIVABLE', 100 + index * 10, 'MANUAL_RECEIVABLE', dateOffset(-1 - (index % 2))) if (fee) fixtures.push(fee) } } const manualResources = [measured[4]].filter(Boolean) if (feeItems.PAYABLE && supplier?.id) { for (const [index, manualResource] of manualResources.entries()) { const fee = await createManualFee(context, manualResource, feeItems.PAYABLE, 'PAYABLE', 80 + index * 10, 'MANUAL_ORDER_FEE', dateOffset(-3 - index), supplier) if (fee) fixtures.push({ ...fee, supplierId: id(supplier.id), supplierName: supplier.name || null, supplierCategory: supplier.category || payableSupplierCategory }) } } // A source whose fee has already been sent is intentionally frozen against // loading-plan membership changes. Use a dedicated measured order for the // container fixture so finance setup never conflicts with that invariant. const containerResource = measured[5] const container = (containerResource ? await createMarkerContainer(context, containerResource) : null) || context.createdResources?.phase3ContainerLoading const containerOwned = (container?.marker === context.marker || String(container?.marker || '').startsWith(`${context.marker}-`)) && container?.loadingPlanId && container?.containerBookingId let containerFixture = null if (containerOwned && feeItems.PAYABLE && supplier?.id) { const basePath = `/api/platform/v1/loading-plans/${encodeURIComponent(container.loadingPlanId)}/fee-details` const path = `${basePath}?accountId=${encodeURIComponent(container.tenantId)}` const body = { feeItemId: String(feeItems.PAYABLE.id), direction: 'PAYABLE', businessDate: dateOffset(-2), amount: 300, currency: 'CNY', supplierId: id(supplier.id), settlementMode: 'ON_ACCOUNT', remark: `${context.marker}-CONTAINER_SHARED` } const response = await request(context, 'POST', path, body) if (response?.http === 404) { blocker(context, '按柜费用来源接口已部署并可路由', `POST ${path} returned HTTP 404 before Controller handling; Firstleg Controller contract is /api/platform/v1/loading-plans/{loadingPlanId}/fee-details, but current Gateway route does not expose /api/platform/v1/loading-plans/**; requestId=${response.requestId || 'missing'}, traceId=${response.traceId || 'missing'}`) } const detail = context.apiSuccess(MODULE, '创建按柜费用来源', response, '', OPTIONS) const feeId = id(detail?.id) if (feeId) { const approval = await request(context, 'POST', `${basePath}/${feeId}/approve?accountId=${encodeURIComponent(container.tenantId)}`); 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, supplierName: supplier?.name || null, supplierCategory: supplier?.category || payableSupplierCategory, containerBookingId: id(container.containerBookingId), containerNo: container.containerNo || null, loadingPlanId: id(container.loadingPlanId) } } else if (!containerOwned) { context.addStep(MODULE, '取得同 marker 柜次用于整柜费用分摊', null, false, '没有同 marker 的已完成柜次和可核验 containerBookingId;不读取历史柜次、不猜测柜号', OPTIONS) context.addCoverageGap(MODULE, 'container-shared-cost-allocation', '按柜添加费用并按订单/运单体积分摊', '当前上下文没有同 marker 的已完成柜次及真实 Firstleg 体积快照,禁止借用历史柜次或模拟 Finance 分摊成功', 'no-marker-owned-container-snapshot') } else { context.addCoverageGap(MODULE, 'container-shared-cost-allocation', '按柜添加费用并按订单/运单体积分摊', '已识别同 marker 的 COMPLETED 柜次,但缺少 PAYABLE 费用项目或符合其显式可空类别约束的 ACTIVE 供应商', 'missing-payable-supplier') } 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 snapshotInput = await verifyContainerCostSnapshotInput(context, customerId, container) const cost = snapshotInput.valid ? await createContainerCost(context, customerId, container, voucherFileId, snapshotInput) : null if (Array.isArray(cost?.allocations)) allocations = cost.allocations else blocker(context, '回读整柜成本分摊结果', `costId=${cost?.costId || 'missing'};Finance 成本详情未返回真实 fin_cost_allocation,禁止从金额和体积自行伪造持久化结果`) } 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'};至少需要两个真实日期以验收按日期排除`) context.assert(MODULE, '取得订单真实运单标识', null, measured.length > 0 && measured.every((item) => item.waybillId || item.shipmentId || item.orderNo), '使用订单域真实 waybillId、shipmentId 或 orderNo,不使用数据库主键冒充运单标识', OPTIONS) const fixtureOrders = measured.map((item) => ({ orderId: id(item.orderId), orderNo: item.orderNo || null, shipmentId: item.shipmentId ? id(item.shipmentId) : null, waybillId: item.waybillId ? id(item.waybillId) : (item.shipmentId ? id(item.shipmentId) : null), inboundId: id(item.inboundId), actualWeightKg: item.actualWeightKg, totalVolumeCbm: item.totalVolumeCbm, businessDate: fixtures.find((fee) => fee.orderId === id(item.orderId))?.businessDate || null })) const fixtureSources = normalizeFinanceFixtureSources(fixtures, fixtureOrders, allocations, customerId, context.marker) const invalidFixtureSources = fixtureSources.filter((source) => !source.feeSourceId || !source.customerId || !source.businessDate || !(source.waybillId || source.shipmentId || source.orderNo) || source.marker !== context.marker || (source.direction === 'PAYABLE' && (!source.supplierId || !source.supplierName || (payableSupplierCategory && source.supplierCategory !== payableSupplierCategory)))) context.assert(MODULE, '输出完整 Finance 来源契约', null, invalidFixtureSources.length === 0, `sources=${fixtureSources.length}, invalid=${invalidFixtureSources.length};字段必须来自本轮费用来源、客户、订单/运单和真实柜次分摊`, OPTIONS) context.createdResources.financeFullFixtures = { marker: context.marker, customerId, prerequisites: { container: containerOwned && Boolean(containerFixture?.feeSourceId) && allocations.length > 0 ? { ready: true, category: null } : { ready: false, category: 'container-prerequisite', evidence: '缺少同 marker 已完成柜次、有效 containerBookingId、按柜费用来源或真实分摊' }, financeSources: fixtureSources.length > 0 && invalidFixtureSources.length === 0 ? { ready: true, category: null } : { ready: false, category: 'finance-source-prerequisite', evidence: '本轮 marker 未产生字段完整的可消费 Finance 费用来源' }, }, orders: fixtureOrders, feeSources: fixtureSources, container: containerFixture ? { containerBookingId: containerFixture.containerBookingId, containerNo: containerFixture.containerNo } : { containerBookingId: null, containerNo: null }, allocations, supplier: supplier?.id ? { supplierId: id(supplier.id), supplierName: supplier.name || null, category: supplier.category || payableSupplierCategory } : 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 }) }