const MODULE = 'finance-extended-fees' const OPTIONS = { scenario: 'finance-extended-real-fee-statuses', deploymentBlocker: true } const FIXED_MARKER = 'FIN-EXT-FEE-20260812' const BASE = '/api/platform/finance/v1' function id(value) { return value == null ? '' : String(value) } 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 key(action) { return `${FIXED_MARKER}-${action}`.slice(0, 128) } function request(context, method, path, body, action) { return context.request(method, path, body, context.token, { 'Idempotency-Key': key(action) }) } function blocker(context, name, evidence, response = null) { context.addStep(MODULE, name, response, false, evidence, OPTIONS) } async function createSixthOrder(context) { const originalMarker = context.marker const original = context.createdResources.orders context.marker = `${FIXED_MARKER}-STATUS6` try { const orders = await import(new URL('./orders.mjs', import.meta.url)) await orders.run(context) const resource = context.createdResources.orders return resource?.marker === context.marker && resource?.orderId && resource?.inboundId ? { ...resource } : null } finally { context.marker = originalMarker if (original) context.createdResources.orders = original else delete context.createdResources.orders } } async function loadFeeItem(context, direction) { const response = await context.request('GET', '/api/platform/v1/fee-items?page=1&pageSize=200') const data = context.apiSuccess(MODULE, `读取 ${direction} ACTIVE 费用项目`, response, '', OPTIONS) return rows(data).find((item) => item?.status === 'ACTIVE' && (direction === 'RECEIVABLE' ? item.warehouseReceivable === true : item.warehousePayable === true)) || null } async function loadActivePartner(context) { const response = await context.request('GET', '/api/platform/v1/partners?page=1&size=200&status=ACTIVE') const data = context.apiSuccess(MODULE, '读取现有 ACTIVE 往来单位', response, '', OPTIONS) return rows(data).find((item) => item?.status === 'ACTIVE') || null } async function createFee(context, resource, item, spec, partner) { const base = `/api/platform/v1/inbounds/${encodeURIComponent(resource.inboundId)}/fee-details` const path = `${base}?accountId=${encodeURIComponent(resource.tenantId)}` const body = { feeItemId: Number(item.id), direction: spec.direction, businessDate: spec.businessDate, unitPrice: spec.amount, currency: 'CNY', amount: spec.amount, settlementIncluded: true, partnerId: spec.direction === 'PAYABLE' ? String(partner.id) : null, partnerName: spec.direction === 'PAYABLE' ? (partner.partnerName || partner.name || null) : null, remark: `${FIXED_MARKER}-${spec.name}`, } const response = await request(context, 'POST', path, body, `create-${spec.name}`) const detail = context.apiSuccess(MODULE, `创建${spec.label}`, response, `orderId=${resource.orderId}, inboundId=${resource.inboundId}`, OPTIONS) const feeId = id(detail?.id) context.assert(MODULE, `${spec.label}初始状态和归属`, response, Boolean(feeId) && id(detail?.inboundId) === id(resource.inboundId) && detail?.reviewStatus === 'PENDING' && Number(detail?.amount) === spec.amount, `feeId=${feeId || 'missing'}, review=${detail?.reviewStatus || 'missing'}, amount=${detail?.amount ?? 'missing'}`, OPTIONS) return feeId ? { ...spec, feeId, base, resource, createResponse: response } : null } async function readOrderInboundAndFee(context, fixture) { const orderResponse = await context.request('GET', `/api/platform/v1/orders/first-leg/${encodeURIComponent(fixture.resource.orderId)}?tenantId=${encodeURIComponent(fixture.resource.tenantId)}`) const order = context.apiSuccess(MODULE, `${fixture.label}回读订单`, orderResponse, `orderId=${fixture.resource.orderId}`, OPTIONS) const inboundResponse = await context.request('GET', `/api/platform/inbounds/${encodeURIComponent(fixture.resource.inboundId)}?tenantId=${encodeURIComponent(fixture.resource.tenantId)}`) const inbound = context.apiSuccess(MODULE, `${fixture.label}回读入库单`, inboundResponse, `inboundId=${fixture.resource.inboundId}`, OPTIONS) const feeResponse = await context.request('GET', `${fixture.base}?accountId=${encodeURIComponent(fixture.resource.tenantId)}`) const feeRows = context.apiSuccess(MODULE, `${fixture.label}回读费用`, feeResponse, '', OPTIONS) const fee = rows(feeRows).find((item) => id(item.id) === fixture.feeId) context.assert(MODULE, `${fixture.label}订单、入库和费用关联一致`, feeResponse, Boolean(order) && Boolean(inbound) && id(fee?.inboundId) === id(fixture.resource.inboundId), `order=${Boolean(order)}, inbound=${Boolean(inbound)}, fee=${Boolean(fee)}`, OPTIONS) return { order, inbound, fee, responses: { orderResponse, inboundResponse, feeResponse } } } async function financeState(context, customerId, fixture) { const response = await context.request('GET', `${BASE}/fee-pool?customerId=${encodeURIComponent(customerId)}&direction=${fixture.direction}`) const data = context.apiSuccess(MODULE, `${fixture.label}回读 Finance 池`, response, '', OPTIONS) const source = rows(data).find((item) => id(item.feeSourceId || item.id) === fixture.feeId) const shouldExist = fixture.expectedFinance === 'AVAILABLE' context.assert(MODULE, `${fixture.label} Finance 池状态`, response, shouldExist ? source?.sourceStatus === 'AVAILABLE' : !source, `expected=${fixture.expectedFinance}, actual=${source?.sourceStatus || 'ABSENT'}, feeSourceId=${fixture.feeId}`, OPTIONS) return { response, source: source || null } } export async function run(context) { const start = context.steps.length const originalMarker = context.marker context.marker = FIXED_MARKER try { const full = await import(new URL('./finance-full-fixtures.mjs', import.meta.url)) await full.run(context) const baseFixture = context.createdResources.financeFullFixtures if (!baseFixture || baseFixture.marker !== FIXED_MARKER) { blocker(context, '生成基础重量/体积/人工费用 fixture', `financeFullFixtures.marker=${baseFixture?.marker || 'missing'};禁止消费其他 marker`) return } const automatic = baseFixture.feeSources.filter((item) => item.sourceType === 'FBA_PRICING') for (const basis of ['WEIGHT_KG', 'VOLUME_CBM']) { context.assert(MODULE, `${basis} 至少两条真实自动费用`, null, automatic.filter((item) => item.billingBasis === basis).length >= 2, `count=${automatic.filter((item) => item.billingBasis === basis).length}`, OPTIONS) } const sixth = await createSixthOrder(context) context.assert(MODULE, '创建第六张独立订单与入库单', null, Boolean(sixth), sixth ? `orderId=${sixth.orderId}, inboundId=${sixth.inboundId}` : 'missing', OPTIONS) const resources = [...baseFixture.orders.map((item) => ({ ...item, tenantId: baseFixture.customerId })), sixth].filter(Boolean) context.assert(MODULE, '本轮至少六张订单/入库单且无重复', null, resources.length >= 6 && new Set(resources.map((item) => id(item.orderId))).size >= 6 && new Set(resources.map((item) => id(item.inboundId))).size >= 6, `orders=${resources.length}, uniqueOrders=${new Set(resources.map((item) => id(item.orderId))).size}, uniqueInbounds=${new Set(resources.map((item) => id(item.inboundId))).size}`, OPTIONS) const receivableItem = await loadFeeItem(context, 'RECEIVABLE') const payableItem = await loadFeeItem(context, 'PAYABLE') const partner = await loadActivePartner(context) if (!receivableItem || !payableItem || !partner?.id) { blocker(context, '费用状态矩阵前置主数据完整', `receivableFeeItem=${id(receivableItem?.id) || 'missing'}, payableFeeItem=${id(payableItem?.id) || 'missing'}, activePartner=${id(partner?.id) || 'missing'}`) return } const specs = [ { name: 'AR-PENDING', label: '手工应收待审核', direction: 'RECEIVABLE', amount: 111, businessDate: dateOffset(-5), action: null, expectedReview: 'PENDING', expectedFinance: 'ABSENT' }, { name: 'AR-REJECTED', label: '手工应收驳回', direction: 'RECEIVABLE', amount: 112, businessDate: dateOffset(-4), action: 'reject', expectedReview: 'REJECTED', expectedFinance: 'ABSENT' }, { name: 'AR-APPROVED', label: '手工应收已审核待出账', direction: 'RECEIVABLE', amount: 113, businessDate: dateOffset(-3), action: 'approve', expectedReview: 'APPROVED', expectedFinance: 'AVAILABLE' }, { name: 'AP-PENDING', label: '手工应付待审核', direction: 'PAYABLE', amount: 81, businessDate: dateOffset(-2), action: null, expectedReview: 'PENDING', expectedFinance: 'ABSENT' }, { name: 'AP-APPROVED', label: '手工应付已审核', direction: 'PAYABLE', amount: 82, businessDate: dateOffset(-1), action: 'approve', expectedReview: 'APPROVED', expectedFinance: 'AVAILABLE' }, { name: 'AR-VOID', label: '手工应收作废', direction: 'RECEIVABLE', amount: 114, businessDate: dateOffset(-6), action: 'approve-void', expectedReview: 'APPROVED', expectedFinance: 'ABSENT' }, ] const fixtures = [] for (const [index, spec] of specs.entries()) { const resource = resources[index % resources.length] const fixture = await createFee(context, resource, spec.direction === 'RECEIVABLE' ? receivableItem : payableItem, spec, partner) if (!fixture) continue if (spec.action === 'reject') { const response = await request(context, 'POST', `${fixture.base}/${fixture.feeId}/reject?accountId=${encodeURIComponent(resource.tenantId)}`, { reason: FIXED_MARKER }, `reject-${spec.name}`) context.apiSuccess(MODULE, `${spec.label}执行驳回`, response, '', OPTIONS) } else if (spec.action?.startsWith('approve')) { const response = await request(context, 'POST', `${fixture.base}/${fixture.feeId}/approve?accountId=${encodeURIComponent(resource.tenantId)}`, undefined, `approve-${spec.name}`) context.apiSuccess(MODULE, `${spec.label}执行审核`, response, '', OPTIONS) if (spec.action === 'approve-void') { const voidResponse = await request(context, 'POST', `${fixture.base}/${fixture.feeId}/void?accountId=${encodeURIComponent(resource.tenantId)}`, { expectedSourceVersion: 1 }, `void-${spec.name}`) context.apiSuccess(MODULE, `${spec.label}执行作废`, voidResponse, '', OPTIONS) } } fixtures.push(fixture) } const conflictTarget = fixtures.find((item) => item.name === 'AR-APPROVED') if (conflictTarget) { const adjustBody = { feeItemId: Number(receivableItem.id), direction: 'RECEIVABLE', businessDate: conflictTarget.businessDate, unitPrice: 115, currency: 'CNY', amount: 115, settlementIncluded: true, partnerId: null, partnerName: null, remark: `${FIXED_MARKER}-CONFLICT` } const conflict = await request(context, 'POST', `${conflictTarget.base}/${conflictTarget.feeId}/adjust?accountId=${encodeURIComponent(conflictTarget.resource.tenantId)}&expectedSourceVersion=999`, adjustBody, 'stale-adjust') context.assert(MODULE, '错误 sourceVersion 调整产生并发版本冲突', conflict, conflict?.body?.code !== '200', `code=${conflict?.body?.code || 'missing'}, requestId=${conflict?.requestId || 'missing'}, traceId=${conflict?.traceId || 'missing'}`, OPTIONS) } const replayTarget = fixtures.find((item) => item.name === 'AR-PENDING') if (replayTarget) { const replayPath = `${replayTarget.base}?accountId=${encodeURIComponent(replayTarget.resource.tenantId)}` const replayBody = { feeItemId: Number(receivableItem.id), direction: 'RECEIVABLE', businessDate: replayTarget.businessDate, unitPrice: 116, currency: 'CNY', amount: 116, settlementIncluded: true, partnerId: null, partnerName: null, remark: `${FIXED_MARKER}-IDEMPOTENT` } const first = await request(context, 'POST', replayPath, replayBody, 'idempotent-create') const second = await request(context, 'POST', replayPath, replayBody, 'idempotent-create') const firstData = first?.body?.data; const secondData = second?.body?.data context.assert(MODULE, '同键同参人工费用创建幂等重放', second, first?.body?.code === '200' && second?.body?.code === '200' && id(firstData?.id) === id(secondData?.id), `first=${id(firstData?.id) || 'missing'}, replay=${id(secondData?.id) || 'missing'}, requestId=${second?.requestId || 'missing'}`, OPTIONS) } const statusMatrix = [] for (const fixture of fixtures) { const read = await readOrderInboundAndFee(context, fixture) context.assert(MODULE, `${fixture.label}最终审核状态`, read.responses.feeResponse, read.fee?.reviewStatus === fixture.expectedReview, `expected=${fixture.expectedReview}, actual=${read.fee?.reviewStatus || 'missing'}, publication=${read.fee?.publicationStatus || 'missing'}`, OPTIONS) const finance = await financeState(context, baseFixture.customerId, fixture) statusMatrix.push({ name: fixture.name, label: fixture.label, orderId: id(fixture.resource.orderId), orderNo: fixture.resource.orderNo || null, inboundId: id(fixture.resource.inboundId), feeId: fixture.feeId, direction: fixture.direction, businessDate: fixture.businessDate, amount: fixture.amount, reviewStatus: read.fee?.reviewStatus || null, publicationStatus: read.fee?.publicationStatus || null, financeState: finance.source?.sourceStatus || 'ABSENT', requestId: finance.response?.requestId || null, traceId: finance.response?.traceId || null }) } const voided = fixtures.find((item) => item.name === 'AR-VOID') if (voided) { const create = await request(context, 'POST', `${BASE}/statements`, { customerId: String(baseFixture.customerId), currency: 'CNY', generationMode: 'SELECTED', feeSourceIds: [voided.feeId], periodStart: voided.businessDate, periodEnd: voided.businessDate, remark: `${FIXED_MARKER}-VOID-STATEMENT`, }, 'voided-statement') context.assert(MODULE, '作废来源不可选择出账', create, create?.body?.code !== '200', `code=${create?.body?.code || 'missing'}, requestId=${create?.requestId || 'missing'}, traceId=${create?.traceId || 'missing'}`, OPTIONS) } const orderFeeCounts = new Map() for (const item of [...baseFixture.feeSources, ...fixtures]) { if (!item.orderId) continue orderFeeCounts.set(id(item.orderId), (orderFeeCounts.get(id(item.orderId)) || 0) + 1) } context.assert(MODULE, '至少一张订单包含多个费用项目', null, [...orderFeeCounts.values()].some((count) => count >= 2), `maxFeeCount=${Math.max(0, ...orderFeeCounts.values())}`, OPTIONS) context.assert(MODULE, '状态矩阵覆盖多个业务日期', null, new Set(statusMatrix.map((item) => item.businessDate)).size >= 4, `dates=${[...new Set(statusMatrix.map((item) => item.businessDate))].join(',')}`, OPTIONS) context.createdResources.financeExtendedFees = { marker: FIXED_MARKER, customerId: id(baseFixture.customerId), orders: resources.map((item) => ({ orderId: id(item.orderId), orderNo: item.orderNo || null, inboundId: id(item.inboundId) })), automaticFees: automatic, statusMatrix, baseDeploymentBlockers: baseFixture.deploymentBlocker || [], } } finally { context.marker = originalMarker 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 }) } }