const MODULE = 'finance' const OPTIONS = { scenario: 'finance-phase1-g1-g4', deploymentBlocker: true } 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 fixture(context) { const candidates = [context.createdResources?.inboundMeasurement, context.createdResources?.testInbound, context.createdResources?.orders, context.context?.testInbound, context.context?.createdInbound] return candidates.find((item) => item?.tenantId && item?.inboundId && item?.marker === context.marker) || null } function key(context, action) { return `${context.marker}-${action}`.slice(0, 128) } function write(context, method, path, body, action) { return context.request(method, path, body, context.token, { 'Idempotency-Key': key(context, action) }) } function statementId(detail) { return id(detail?.statementId ?? detail?.id ?? detail?.billId) } function settlementId(detail) { return id(detail?.summary?.settlementId ?? detail?.summary?.id ?? detail?.settlementId ?? detail?.id) } export async function run(context) { const start = context.steps.length const openapiResponse = await context.request('GET', '/finance-service/v3/api-docs') const openapi = openapiResponse.body context.addStep(MODULE, 'Gateway 可访问 Finance OpenAPI', openapiResponse, openapiResponse.http === 200 && typeof openapi?.openapi === 'string', '/finance-service/v3/api-docs', OPTIONS) context.assert(MODULE, 'OpenAPI 暴露 G3/G4 冻结路径', openapiResponse, Boolean(openapi?.paths?.[`${BASE}/statements`] && openapi?.paths?.[`${BASE}/receivable-settlements`] && openapi?.paths?.[`${BASE}/settlements/{settlementId}/transactions`]), `paths=${Object.keys(openapi?.paths || {}).length}`, OPTIONS) const accountsResponse = await context.request('GET', `${BASE}/accounts?page=1&pageSize=20`) const accounts = context.apiSuccess(MODULE, '查询客户账户派生汇总', accountsResponse, '', OPTIONS) context.assert(MODULE, '客户账户不暴露钱包余额字段', accountsResponse, rows(accounts).every((item) => !Object.hasOwn(item, 'balance') && !Object.hasOwn(item, 'rechargeAmount') && !Object.hasOwn(item, 'prepayment')), `records=${rows(accounts).length}`, OPTIONS) const current = fixture(context) if (!current) { context.addCoverageGap(MODULE, 'finance-owned-fixture', 'G2-G4 写操作真实验收', '本轮未取得同 marker 的客户入库单,禁止借用历史费用来源执行不可逆财务写操作', 'no-marker-owned-fixture') return } const customerId = id(current.tenantId) const inboundId = id(current.inboundId) const feePoolResponse = await context.request('GET', `${BASE}/fee-pool?customerId=${encodeURIComponent(customerId)}&direction=RECEIVABLE`) const feePool = context.apiSuccess(MODULE, '查询本轮客户应收费用来源', feePoolResponse, `customerId=${customerId}, inboundId=${inboundId}`, OPTIONS) const source = rows(feePool).find((item) => id(item.inboundId) === inboundId && id(item.feeSourceId || item.id)) context.assert(MODULE, '费用来源仅使用本轮入库单', feePoolResponse, Boolean(source), source ? `feeSourceId=${id(source.feeSourceId || source.id)}` : '未找到本轮入库单费用来源', OPTIONS) if (!source) return const feeSourceId = id(source.feeSourceId || source.id) let detailResponse = await context.request('GET', `${BASE}/fee-sources/${encodeURIComponent(feeSourceId)}`) let detail = context.apiSuccess(MODULE, '读取本轮费用来源详情', detailResponse, '', OPTIONS) if (detail?.reviewStatus === 'DRAFT' || detail?.reviewStatus === 'REJECTED') { const submitBody = { reason: context.marker, expectedVersion: Number(detail.operationVersion ?? detail.version) } detailResponse = await write(context, 'POST', `${BASE}/fee-sources/${encodeURIComponent(feeSourceId)}/submit`, submitBody, 'fee-submit') detail = context.apiSuccess(MODULE, '提交本轮费用来源审核', detailResponse, '', OPTIONS) } if (detail?.reviewStatus === 'PENDING') { const reviewBody = { action: 'APPROVE', reason: context.marker, expectedVersion: Number(detail.operationVersion ?? detail.version) } detailResponse = await write(context, 'POST', `${BASE}/fee-sources/${encodeURIComponent(feeSourceId)}/review`, reviewBody, 'fee-review') detail = context.apiSuccess(MODULE, '审核本轮费用来源', detailResponse, '', OPTIONS) } context.assert(MODULE, '本轮来源满足对账单生成条件', detailResponse, detail?.reviewStatus === 'APPROVED' && detail?.sourceStatus === 'AVAILABLE' && detail?.currency === 'CNY', `review=${detail?.reviewStatus}, status=${detail?.sourceStatus}`, OPTIONS) const businessDate = detail?.businessDate || new Date().toISOString().slice(0, 10) const createBody = { customerId, currency: 'CNY', generationMode: 'SELECTED', feeSourceIds: [feeSourceId], periodStart: businessDate, periodEnd: businessDate, remark: context.marker } const createResponse = await write(context, 'POST', `${BASE}/statements`, createBody, 'statement-create') let statement = context.apiSuccess(MODULE, '按本轮来源生成对账单', createResponse, '', OPTIONS) const createdStatementId = statementId(statement) const replayResponse = await write(context, 'POST', `${BASE}/statements`, createBody, 'statement-create') const replay = context.apiSuccess(MODULE, '同键同参生成对账单重放原结果', replayResponse, '', OPTIONS) context.assert(MODULE, '对账单幂等重放保持同一资源', replayResponse, Boolean(createdStatementId) && statementId(replay) === createdStatementId, `first=${createdStatementId || 'missing'}, replay=${statementId(replay) || 'missing'}`, OPTIONS) if (!createdStatementId) return const permissionResponse = await context.request('GET', '/api/platform/v1/iam/authorization/permissions') const permissions = context.apiSuccess(MODULE, '客户详情财务账单查看权限链可用', permissionResponse, '', OPTIONS) const grantedPermissions = new Set(rows(permissions).map((item) => id(item?.permissionCode ?? item))) context.assert(MODULE, '财务账单列表查看权限已授予', permissionResponse, grantedPermissions.has('platform:finance:customer-statement:list'), `permissions=${[...grantedPermissions].filter((item) => item.includes('finance')).join(',')}`, OPTIONS) const statementListResponse = await context.request('GET', `${BASE}/statements?customerId=${encodeURIComponent(customerId)}`) const statementList = context.apiSuccess(MODULE, '客户详情财务账单列表按客户过滤', statementListResponse, `customerId=${customerId}`, OPTIONS) const statementRows = rows(statementList) context.assert(MODULE, '财务账单列表仅返回本轮客户且包含新账单', statementListResponse, statementRows.length > 0 && statementRows.every((item) => id(item?.customerId) === customerId) && statementRows.some((item) => statementId(item) === createdStatementId), `customerId=${customerId}, statementId=${createdStatementId}, rows=${statementRows.length}`, OPTIONS) const statementDetailResponse = await context.request('GET', `${BASE}/reconciliation-statements/${encodeURIComponent(createdStatementId)}`) const statementDetail = context.apiSuccess(MODULE, '读取客户详情财务账单详情', statementDetailResponse, `statementId=${createdStatementId}`, OPTIONS) context.assert(MODULE, '财务账单详情客户与明细修订结构完整', statementDetailResponse, id(statementDetail?.customerId) === customerId && statementId(statementDetail) === createdStatementId && Array.isArray(statementDetail?.lines) && Array.isArray(statementDetail?.revisions), `customerId=${id(statementDetail?.customerId) || 'missing'}, statementId=${statementId(statementDetail) || 'missing'}, lines=${Array.isArray(statementDetail?.lines)}, revisions=${Array.isArray(statementDetail?.revisions)}`, OPTIONS) for (const action of ['SUBMIT', 'APPROVE', 'ISSUE']) { const actionResponse = await write(context, 'POST', `${BASE}/reconciliation-statements/${createdStatementId}/actions`, { action, expectedVersion: Number(statement.version) }, `statement-${action.toLowerCase()}`) statement = context.apiSuccess(MODULE, `对账单执行 ${action}`, actionResponse, '', OPTIONS) } const adjustmentLines = rows(statement?.lines).map((line) => ({ statementLineId: id(line.statementLineId || line.id), adjustedAmount: Number(line.amount), reason: context.marker })) const adjustResponse = await write(context, 'POST', `${BASE}/reconciliation-statements/${createdStatementId}/actions`, { action: 'ADJUST', reason: context.marker, expectedVersion: Number(statement.version), adjustmentLines }, 'statement-adjust') statement = context.apiSuccess(MODULE, '对账单多行调整并保留 revision', adjustResponse, '', OPTIONS) context.assert(MODULE, '调整响应包含修订历史', adjustResponse, adjustmentLines.length > 0 && rows(statement?.revisions).length > 0, `lines=${adjustmentLines.length}, revisions=${rows(statement?.revisions).length}`, OPTIONS) const arBody = { reconciliationStatementId: createdStatementId, currency: 'CNY' } const arResponse = await write(context, 'POST', `${BASE}/receivable-settlements`, arBody, 'settlement-ar-create') const ar = context.apiSuccess(MODULE, '从已出账对账单生成应收结算单', arResponse, '', OPTIONS) const arId = settlementId(ar) const arDetailResponse = arId ? await context.request('GET', `${BASE}/settlements/${arId}`) : arResponse const arDetail = arId ? context.apiSuccess(MODULE, '查询应收结算单详情', arDetailResponse, '', OPTIONS) : null context.assert(MODULE, '应收结算单方向和金额正确', arDetailResponse, Boolean(arId) && arDetail?.summary?.direction === 'RECEIVABLE' && Number(arDetail?.summary?.totalAmount) === Number(statement.totalAmount), `settlementId=${arId || 'missing'}, direction=${arDetail?.summary?.direction || 'missing'}`, OPTIONS) const apListResponse = await context.request('GET', `${BASE}/payable-settlements?customerId=${encodeURIComponent(customerId)}`) context.apiSuccess(MODULE, '查询本轮客户应付结算单列表', apListResponse, '', OPTIONS) context.addCoverageGap(MODULE, 'payable-payment', '应付生成、部分付款与付清', '本轮上游 fixture 未承诺生成同客户同供应商 PAYABLE 来源;禁止借用历史应付单登记付款', 'no-marker-owned-payable-source') context.addCoverageGap(MODULE, 'receipt-three-settlements', '300 元收款分配 3 张 100 元应收单', '当前模块只有一条本轮独占应收来源,无法安全构造三张可清理应收单', 'insufficient-marker-owned-receivable-sources') context.createdResources.finance = { marker: context.marker, customerId, inboundId, feeSourceId, statementId: createdStatementId, receivableSettlementId: arId || null } 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 }) }