外包风险评估系统:领域引擎+前端+服务端持久化与生产部署

- 确定性领域引擎(分类/评分/分级/红线/费用/裁决)+LLM(通义千问)语言理解
- 6步评估向导、服务端草稿持久化(跨设备/编辑草稿保护)
- 工作流(草稿→风控→管理层)、RBAC、报告导出、校准、客户/费率/红线/最低工资管理
- 专业图标体系替换全部emoji、看板美化
- 生产化:API_BASE可配置(同源反代)、auth密钥惰性读取修复RBAC
- 444单测+204前端测试+51 e2e
This commit is contained in:
freedakgmail
2026-06-13 01:06:39 +08:00
commit c670b9e454
404 changed files with 61820 additions and 0 deletions
@@ -0,0 +1,78 @@
/**
* Property 31: 风险溢价区间合法且随分级单调 的属性化测试(Cost_EngineReq 8.1)。
*
* 属性陈述:对任意 Risk_Grade 与 Risk_Score`estimate` 输出的 riskPremiumRange 满足:
* - 区间合法:lower ≤ upper(下界恒不大于上界);
* - 单位恒为"百分比"
* - 随分级单调非减:沿严重度升序 低 < 中 < 高 < 极高,相邻分级的下界与上界均逐级
* 非减(更高分级溢价恒不低于更低分级)。
*
* 本测试以智能生成器构造任意 Risk_Score(0–100 整数,覆盖端点与区间内取值)与任意
* Risk_Grade,并对相邻分级对在相同 score 下比较端点,约束跨分级单调性。
*
* Feature: outsourcing-risk-assessment, Property 31: 风险溢价区间合法且随分级单调
* Validates: Requirements 8.1
*/
import { describe, expect, it } from 'vitest';
import fc from 'fast-check';
import { estimate } from '../index.js';
import {
RISK_GRADE_VALUES,
type RiskGrade,
type RiskScore,
} from '../../domain/common.js';
// ----------------------------------------------------------------------------
// 生成器:Risk_Score 覆盖 [0,100] 整数(含端点);Risk_Grade 取四级之一。
// ----------------------------------------------------------------------------
/** Risk_Score0 至 100 的整数,覆盖边界与区间内值。 */
const riskScoreArb: fc.Arbitrary<RiskScore> = fc.integer({ min: 0, max: 100 });
/** Risk_Grade:四级之一。 */
const riskGradeArb: fc.Arbitrary<RiskGrade> = fc.constantFrom(...RISK_GRADE_VALUES);
/** 严重度升序索引:低 < 中 < 高 < 极高。 */
const gradeOrder: readonly RiskGrade[] = RISK_GRADE_VALUES;
describe('Property 31: 风险溢价区间合法且随分级单调 (Req 8.1)', () => {
it('对任意分级与评分,riskPremiumRange 合法(lower ≤ upper,单位为百分比)', () => {
fc.assert(
fc.property(riskGradeArb, riskScoreArb, (grade, score) => {
const { riskPremiumRange } = estimate(
{ riskGrade: grade, riskScore: score },
{},
);
expect(riskPremiumRange.lower).toBeLessThanOrEqual(riskPremiumRange.upper);
expect(riskPremiumRange.unit).toBe('百分比');
}),
{ numRuns: 100 },
);
});
it('溢价区间随分级单调非减:更高分级的下界与上界均 ≥ 更低分级', () => {
fc.assert(
fc.property(riskScoreArb, riskScoreArb, (lowerScore, higherScore) => {
// 沿严重度升序逐级比较相邻分级的端点。
for (let i = 1; i < gradeOrder.length; i += 1) {
const lowerGrade = gradeOrder[i - 1] as RiskGrade;
const higherGrade = gradeOrder[i] as RiskGrade;
const lowerRange = estimate(
{ riskGrade: lowerGrade, riskScore: lowerScore },
{},
).riskPremiumRange;
const higherRange = estimate(
{ riskGrade: higherGrade, riskScore: higherScore },
{},
).riskPremiumRange;
expect(higherRange.lower).toBeGreaterThanOrEqual(lowerRange.lower);
expect(higherRange.upper).toBeGreaterThanOrEqual(lowerRange.upper);
}
}),
{ numRuns: 100 },
);
});
});
@@ -0,0 +1,139 @@
/**
* Property 32: 费用项非负且标注依据 的属性化测试(Cost_EngineReq 8.2, 8.6)。
*
* 属性陈述:对任意成本输入,垫资利息、保险费用、补偿准备金、坏账准备金各项金额
* 恒非负,且每一项测算金额必标注其所依据的输入项(basisInputs 非空)与所采用的
* 费率或参数来源(rateSource 非空)。
*
* 本测试在已完成评分(riskScore ∈ [0,100]、riskGrade 取四级之一)的前提下,生成
* 任意成本输入项——包括缺省(undefined)、零、负数、小数与超大金额/费率等边界,
* 以验证:
* 1. 四项命名费用项金额恒非负(即便输入为负,roundMoney 兜底为非负)。
* 2. breakdown 中每一项测算金额均标注非空的 basisInputs 与非空的 rateSource。
*
* Feature: outsourcing-risk-assessment, Property 32: 费用项非负且标注依据
* Validates: Requirements 8.2, 8.6
*/
import { describe, expect, it } from 'vitest';
import fc from 'fast-check';
import { RISK_GRADE_VALUES } from '../../domain/common.js';
import { estimate, type CostInputs, type ScoringSnapshot } from '../estimate.js';
// ----------------------------------------------------------------------------
// 生成器
// ----------------------------------------------------------------------------
/** 评分快照:评分已完成(riskScore 0..100 整数、riskGrade 取四级之一)。 */
const scoringArb: fc.Arbitrary<ScoringSnapshot> = fc.record({
riskScore: fc.integer({ min: 0, max: 100 }),
riskGrade: fc.constantFrom(...RISK_GRADE_VALUES),
});
/**
* 金额生成器:覆盖零、正负小数与超大金额等边界。
* 含负值用于验证"即便输入异常,费用项金额仍兜底为非负"。
*/
const moneyArb = fc.double({
min: -1_000_000,
max: 10_000_000,
noNaN: true,
noDefaultInfinity: true,
});
/** 费率/比例生成器(小数):覆盖零、负值与较大费率等边界。 */
const rateArb = fc.double({
min: -1,
max: 5,
noNaN: true,
noDefaultInfinity: true,
});
/** 账期月数生成器:覆盖零、负值与较长周期。 */
const monthsArb = fc.double({
min: -12,
max: 120,
noNaN: true,
noDefaultInfinity: true,
});
/** 仅在值已定义时写入键,以兼容 exactOptionalPropertyTypes 下的可选属性语义。 */
function assignDefined<K extends keyof CostInputs>(
target: CostInputs,
key: K,
value: number | undefined,
): void {
if (value !== undefined) {
target[key] = value;
}
}
/**
* 任意成本输入:每个字段以 option 生成,缺省时省略该键(触发默认值兜底)。
* 通过仅写入已定义的键,满足 exactOptionalPropertyTypes 的精确可选属性约束。
*/
const costInputsArb: fc.Arbitrary<CostInputs> = fc
.record({
baselineQuote: fc.option(moneyArb, { nil: undefined }),
advancePrincipal: fc.option(moneyArb, { nil: undefined }),
advanceMonths: fc.option(monthsArb, { nil: undefined }),
annualInterestRate: fc.option(rateArb, { nil: undefined }),
insuredAmount: fc.option(moneyArb, { nil: undefined }),
insuranceRate: fc.option(rateArb, { nil: undefined }),
compensationBase: fc.option(moneyArb, { nil: undefined }),
compensationReserveRate: fc.option(rateArb, { nil: undefined }),
badDebtBase: fc.option(moneyArb, { nil: undefined }),
badDebtRate: fc.option(rateArb, { nil: undefined }),
})
.map((raw) => {
const inputs: CostInputs = {};
assignDefined(inputs, 'baselineQuote', raw.baselineQuote);
assignDefined(inputs, 'advancePrincipal', raw.advancePrincipal);
assignDefined(inputs, 'advanceMonths', raw.advanceMonths);
assignDefined(inputs, 'annualInterestRate', raw.annualInterestRate);
assignDefined(inputs, 'insuredAmount', raw.insuredAmount);
assignDefined(inputs, 'insuranceRate', raw.insuranceRate);
assignDefined(inputs, 'compensationBase', raw.compensationBase);
assignDefined(inputs, 'compensationReserveRate', raw.compensationReserveRate);
assignDefined(inputs, 'badDebtBase', raw.badDebtBase);
assignDefined(inputs, 'badDebtRate', raw.badDebtRate);
return inputs;
});
// ----------------------------------------------------------------------------
// 属性测试
// ----------------------------------------------------------------------------
describe('Property 32: 费用项非负且标注依据 (Req 8.2, 8.6)', () => {
it('四项命名费用项金额恒非负,且每项拆解均标注依据输入项与费率/参数来源', () => {
fc.assert(
fc.property(scoringArb, costInputsArb, (scoring, costInputs) => {
const result = estimate(scoring, costInputs);
// 1. 四项命名费用项金额恒非负(Req 8.2)。
expect(result.advanceInterest).toBeGreaterThanOrEqual(0);
expect(result.insuranceCost).toBeGreaterThanOrEqual(0);
expect(result.compensationReserve).toBeGreaterThanOrEqual(0);
expect(result.badDebtReserve).toBeGreaterThanOrEqual(0);
// 2. 每一项测算金额必标注其依据(Req 8.2, 8.6)。
expect(result.breakdown.length).toBeGreaterThan(0);
for (const item of result.breakdown) {
// 测算金额非负。
expect(item.amount).toBeGreaterThanOrEqual(0);
// 标注所依据的输入项:非空数组且每项为非空字符串。
expect(Array.isArray(item.basisInputs)).toBe(true);
expect(item.basisInputs.length).toBeGreaterThan(0);
for (const basis of item.basisInputs) {
expect(typeof basis).toBe('string');
expect(basis.length).toBeGreaterThan(0);
}
// 标注费率/参数来源:非空字符串。
expect(typeof item.rateSource).toBe('string');
expect(item.rateSource.length).toBeGreaterThan(0);
}
}),
{ numRuns: 100 },
);
});
});
@@ -0,0 +1,107 @@
/**
* Property 33: 风险调整后报价不低于基准且拆解一致(Req 8.3)。
*
* 属性陈述:
* 对任意合法的评分快照(Risk_Score ∈ [0,100]、Risk_Grade ∈ {低,中,高,极高})与任意
* 成本输入项组合(各字段可缺失),`estimate` 输出的费用测算结果满足:
* - 风险调整后报价恒不低于基准报价(riskAdjustedQuote ≥ baselineQuote);
* - 费用拆解明细各项金额之和与风险调整后报价口径一致(在两位小数舍入容差内相等)。
*
* Feature: outsourcing-risk-assessment, Property 33: 风险调整后报价不低于基准且拆解一致
* Validates: Requirements 8.3
*/
import { describe, expect, it } from 'vitest';
import fc from 'fast-check';
import { RISK_GRADE_VALUES, type RiskGrade } from '../../domain/common.js';
import { estimate, type CostInputs, type ScoringSnapshot } from '../estimate.js';
// ----------------------------------------------------------------------------
// 生成器:合法评分快照与任意(含缺失字段)成本输入项。
// ----------------------------------------------------------------------------
/** 非负有限货币金额(元)。 */
const moneyArb: fc.Arbitrary<number> = fc.double({
min: 0,
max: 10_000_000,
noNaN: true,
noDefaultInfinity: true,
});
/** 费率/比例(小数,[0,1])。 */
const rateArb: fc.Arbitrary<number> = fc.double({
min: 0,
max: 1,
noNaN: true,
noDefaultInfinity: true,
});
/** 评分快照:Risk_Score ∈ [0,100] 整数,Risk_Grade 取四级之一。 */
const scoringArb: fc.Arbitrary<ScoringSnapshot> = fc.record({
riskScore: fc.integer({ min: 0, max: 100 }),
riskGrade: fc.constantFrom<RiskGrade>(...RISK_GRADE_VALUES),
});
/**
* 任意成本输入项:每个字段独立地“提供或缺失”。
* 遵循 exactOptionalPropertyTypes:仅在有值时写入键,避免显式 undefined。
*/
const costInputsArb: fc.Arbitrary<CostInputs> = fc
.record({
baselineQuote: fc.option(moneyArb, { nil: undefined }),
advancePrincipal: fc.option(moneyArb, { nil: undefined }),
advanceMonths: fc.option(fc.integer({ min: 0, max: 36 }), { nil: undefined }),
annualInterestRate: fc.option(rateArb, { nil: undefined }),
insuredAmount: fc.option(moneyArb, { nil: undefined }),
insuranceRate: fc.option(rateArb, { nil: undefined }),
compensationBase: fc.option(moneyArb, { nil: undefined }),
compensationReserveRate: fc.option(rateArb, { nil: undefined }),
badDebtBase: fc.option(moneyArb, { nil: undefined }),
badDebtRate: fc.option(rateArb, { nil: undefined }),
})
.map((fields) => {
const inputs: CostInputs = {};
if (fields.baselineQuote !== undefined) inputs.baselineQuote = fields.baselineQuote;
if (fields.advancePrincipal !== undefined) {
inputs.advancePrincipal = fields.advancePrincipal;
}
if (fields.advanceMonths !== undefined) inputs.advanceMonths = fields.advanceMonths;
if (fields.annualInterestRate !== undefined) {
inputs.annualInterestRate = fields.annualInterestRate;
}
if (fields.insuredAmount !== undefined) inputs.insuredAmount = fields.insuredAmount;
if (fields.insuranceRate !== undefined) inputs.insuranceRate = fields.insuranceRate;
if (fields.compensationBase !== undefined) {
inputs.compensationBase = fields.compensationBase;
}
if (fields.compensationReserveRate !== undefined) {
inputs.compensationReserveRate = fields.compensationReserveRate;
}
if (fields.badDebtBase !== undefined) inputs.badDebtBase = fields.badDebtBase;
if (fields.badDebtRate !== undefined) inputs.badDebtRate = fields.badDebtRate;
return inputs;
});
describe('Property 33: 风险调整后报价不低于基准且拆解一致', () => {
it('riskAdjustedQuote ≥ baselineQuote 且拆解之和与报价口径一致(Req 8.3)', () => {
fc.assert(
fc.property(scoringArb, costInputsArb, (scoring, costInputs) => {
const result = estimate(scoring, costInputs);
// --- 风险调整后报价恒不低于基准报价 ---
expect(Number.isFinite(result.baselineQuote)).toBe(true);
expect(Number.isFinite(result.riskAdjustedQuote)).toBe(true);
expect(result.riskAdjustedQuote).toBeGreaterThanOrEqual(result.baselineQuote);
// --- 拆解之和与风险调整后报价口径一致(两位小数舍入容差内)---
const breakdownSum = result.breakdown.reduce(
(sum, item) => sum + item.amount,
0,
);
// 每项与报价均已规整为两位小数;容差覆盖累加的最小舍入误差。
expect(Math.abs(breakdownSum - result.riskAdjustedQuote)).toBeLessThanOrEqual(0.01);
}),
{ numRuns: 100 },
);
});
});
@@ -0,0 +1,113 @@
/**
* Property 35: 评分未完成拒绝费用测算 的属性化测试(Cost_EngineReq 8.5)。
*
* 属性陈述:*对任意*评分快照,只要缺少 Risk_Score 或 Risk_Grade(评分尚未完成),
* Cost_Engine *必*拒绝执行费用测算并抛出 {@link ScoringNotCompleteError}"评分未完成");
* 当 Risk_Score 与 Risk_Grade 均已产生时,*不会*因评分未完成而抛错。
*
* 本测试以智能生成器分别构造两类评分快照:
* - 未完成:随机决定 riskScore / riskGrade 是否存在,并约束二者至少一项缺失
* (缺失字段整体省略键,契合 exactOptionalPropertyTypes)。断言 estimate 抛出
* ScoringNotCompleteError,且其 userMessage 含"评分未完成"Req 8.5)。
* - 已完成:riskScore 与 riskGrade 均存在。断言 estimate 不抛出 ScoringNotCompleteError
* (携带任意成本输入或不携带均成立)。
*
* Feature: outsourcing-risk-assessment, Property 35: 评分未完成拒绝费用测算
* Validates: Requirements 8.5
*/
import { describe, expect, it } from 'vitest';
import fc from 'fast-check';
import { estimate, ScoringNotCompleteError, type ScoringSnapshot } from '../index.js';
import { RISK_GRADE_VALUES, type RiskGrade } from '../../domain/common.js';
// ----------------------------------------------------------------------------
// 生成器与构造辅助。
// ----------------------------------------------------------------------------
/** Risk_Score 取值(0~100 整数)。 */
const riskScoreArb: fc.Arbitrary<number> = fc.integer({ min: 0, max: 100 });
/** Risk_Grade 取值(四级之一)。 */
const riskGradeArb: fc.Arbitrary<RiskGrade> = fc.constantFrom<RiskGrade>(
...RISK_GRADE_VALUES,
);
/**
* 依"是否存在"按需构造评分快照:仅在存在时写入对应键(缺失则整体省略键,
* 契合 exactOptionalPropertyTypes 与 estimate 的 `=== undefined` 前置校验)。
*/
function buildSnapshot(
scorePresent: boolean,
scoreValue: number,
gradePresent: boolean,
gradeValue: RiskGrade,
): ScoringSnapshot {
const snapshot: ScoringSnapshot = {};
if (scorePresent) {
snapshot.riskScore = scoreValue;
}
if (gradePresent) {
snapshot.riskGrade = gradeValue;
}
return snapshot;
}
/** 评分未完成快照:约束 Risk_Score / Risk_Grade 至少一项缺失。 */
const incompleteSnapshotArb: fc.Arbitrary<ScoringSnapshot> = fc
.record({
scorePresent: fc.boolean(),
gradePresent: fc.boolean(),
scoreValue: riskScoreArb,
gradeValue: riskGradeArb,
})
.filter((plan) => !(plan.scorePresent && plan.gradePresent))
.map((plan) =>
buildSnapshot(plan.scorePresent, plan.scoreValue, plan.gradePresent, plan.gradeValue),
);
/** 评分已完成快照:Risk_Score 与 Risk_Grade 均存在。 */
const completeSnapshotArb: fc.Arbitrary<ScoringSnapshot> = fc.record({
riskScore: riskScoreArb,
riskGrade: riskGradeArb,
});
// ----------------------------------------------------------------------------
// 属性。
// ----------------------------------------------------------------------------
describe('Property 35: 评分未完成拒绝费用测算 (Req 8.5)', () => {
it('任意评分未完成快照(缺少 Risk_Score 或 Risk_Grade):estimate 抛 ScoringNotCompleteError', () => {
fc.assert(
fc.property(incompleteSnapshotArb, (snapshot) => {
// 前置:至少一项缺失。
expect(
snapshot.riskScore === undefined || snapshot.riskGrade === undefined,
).toBe(true);
let thrown: unknown;
try {
estimate(snapshot);
} catch (error) {
thrown = error;
}
expect(thrown).toBeInstanceOf(ScoringNotCompleteError);
expect((thrown as ScoringNotCompleteError).userMessage).toContain(
'评分未完成',
);
}),
{ numRuns: 100 },
);
});
it('任意评分已完成快照(Risk_Score 与 Risk_Grade 均存在):estimate 不因评分未完成而抛错', () => {
fc.assert(
fc.property(completeSnapshotArb, (snapshot) => {
expect(() => estimate(snapshot)).not.toThrow(ScoringNotCompleteError);
// 携带空成本输入亦不应因评分未完成而抛错。
expect(() => estimate(snapshot, {})).not.toThrow(ScoringNotCompleteError);
}),
{ numRuns: 100 },
);
});
});
@@ -0,0 +1,247 @@
/**
* Property 34: 缺失成本输入兜底为假设 的属性化测试(Cost_EngineReq 8.4)。
*
* 属性陈述:*对任意*缺失的成本输入项,Cost_Engine *必*采用行业默认值并将该输入项所
* 影响的费用拆解明细项的 Data_Provenance 标注为"智能体假设"。
*
* 本测试以智能生成器为 10 个成本输入字段各随机决定"是否提供"及其取值,仅将"提供"的
* 字段写入 CostInputs(缺失字段整体省略键,契合 exactOptionalPropertyTypes)。对每个
* 生成场景断言:
* - 行业默认值被采纳:将缺失字段显式补齐为其行业默认值后重算,所有拆解项金额与缺失
* 场景逐项相等 —— 证明缺失字段确实取了行业默认值(Req 8.4 前半);
* - 来源标注为"智能体假设":对每个缺失字段,其所依赖的拆解明细项 provenance 必为
* "智能体假设"Req 8.4 后半);
* - 反向校验:当全部字段均提供时,无任何拆解项被标为"智能体假设"。
*
* Feature: outsourcing-risk-assessment, Property 34: 缺失成本输入兜底为假设
* Validates: Requirements 8.4
*/
import { describe, expect, it } from 'vitest';
import fc from 'fast-check';
import {
estimate,
type CostInputs,
type ScoringSnapshot,
DEFAULT_ADVANCE_MONTHS,
DEFAULT_ANNUAL_INTEREST_RATE,
DEFAULT_BAD_DEBT_RATE,
DEFAULT_BASELINE_QUOTE,
DEFAULT_COMPENSATION_RESERVE_RATE,
DEFAULT_INSURANCE_RATE,
} from '../index.js';
import { ASSUMED_PROVENANCE } from '../../domain/provenance.js';
import { RISK_GRADE_VALUES, type RiskGrade } from '../../domain/common.js';
// ----------------------------------------------------------------------------
// 字段与依赖映射。
// ----------------------------------------------------------------------------
/** 全部成本输入字段。 */
const FIELDS = [
'baselineQuote',
'advancePrincipal',
'advanceMonths',
'annualInterestRate',
'insuredAmount',
'insuranceRate',
'compensationBase',
'compensationReserveRate',
'badDebtBase',
'badDebtRate',
] as const;
type CostField = (typeof FIELDS)[number];
/** 每个输入字段所影响的费用拆解明细项名称(缺失该字段时这些项应标"智能体假设")。 */
const DEPENDENT_ITEMS: Readonly<Record<CostField, readonly string[]>> = {
// 基准报价为各项基数;其缺失直接影响"基准报价"与"风险溢价"两项的来源标注。
baselineQuote: ['基准报价', '风险溢价'],
advancePrincipal: ['垫资利息'],
advanceMonths: ['垫资利息'],
annualInterestRate: ['垫资利息'],
insuredAmount: ['保险费用'],
insuranceRate: ['保险费用'],
compensationBase: ['补偿准备金'],
compensationReserveRate: ['补偿准备金'],
badDebtBase: ['坏账准备金'],
badDebtRate: ['坏账准备金'],
};
/** 单字段计划:是否提供该输入,以及(提供时)其取值。 */
interface FieldPlan {
present: boolean;
value: number;
}
type Plan = Record<CostField, FieldPlan>;
/**
* 字段对应的行业默认值。基数类字段(垫资本金/投保基数/补偿基数/坏账基数)的默认值
* 回退至有效基准报价 effBaseline。
*/
function defaultFor(field: CostField, effBaseline: number): number {
switch (field) {
case 'baselineQuote':
return DEFAULT_BASELINE_QUOTE;
case 'advanceMonths':
return DEFAULT_ADVANCE_MONTHS;
case 'annualInterestRate':
return DEFAULT_ANNUAL_INTEREST_RATE;
case 'insuranceRate':
return DEFAULT_INSURANCE_RATE;
case 'compensationReserveRate':
return DEFAULT_COMPENSATION_RESERVE_RATE;
case 'badDebtRate':
return DEFAULT_BAD_DEBT_RATE;
case 'advancePrincipal':
case 'insuredAmount':
case 'compensationBase':
case 'badDebtBase':
return effBaseline;
}
}
/** 有效基准报价:提供则取之,否则采用默认基准报价。 */
function effectiveBaseline(plan: Plan): number {
return plan.baselineQuote.present
? plan.baselineQuote.value
: DEFAULT_BASELINE_QUOTE;
}
/** 仅将"提供"的字段写入 CostInputs(缺失字段整体省略键)。 */
function buildInputs(plan: Plan): CostInputs {
const inputs: CostInputs = {};
for (const field of FIELDS) {
if (plan[field].present) {
inputs[field] = plan[field].value;
}
}
return inputs;
}
/** 将缺失字段显式补齐为其行业默认值,提供字段保持原值(参照对照组)。 */
function buildFullyProvidedInputs(plan: Plan): CostInputs {
const effBaseline = effectiveBaseline(plan);
const inputs: CostInputs = {};
for (const field of FIELDS) {
inputs[field] = plan[field].present
? plan[field].value
: defaultFor(field, effBaseline);
}
return inputs;
}
// ----------------------------------------------------------------------------
// 生成器。
// ----------------------------------------------------------------------------
/** 金额/基数类取值(非负有限数)。 */
const moneyArb: fc.Arbitrary<number> = fc.double({
min: 0,
max: 1e9,
noNaN: true,
noDefaultInfinity: true,
});
/** 费率/比例类取值([0,1] 小数)。 */
const rateArb: fc.Arbitrary<number> = fc.double({
min: 0,
max: 1,
noNaN: true,
noDefaultInfinity: true,
});
/** 账期月数(非负整数)。 */
const monthsArb: fc.Arbitrary<number> = fc.integer({ min: 0, max: 36 });
/** 各字段对应的取值生成器。 */
function valueArbFor(field: CostField): fc.Arbitrary<number> {
switch (field) {
case 'advanceMonths':
return monthsArb;
case 'annualInterestRate':
case 'insuranceRate':
case 'compensationReserveRate':
case 'badDebtRate':
return rateArb;
default:
return moneyArb;
}
}
/** 单字段计划生成器:随机决定是否提供及取值。 */
function fieldPlanArb(field: CostField): fc.Arbitrary<FieldPlan> {
return fc.record({ present: fc.boolean(), value: valueArbFor(field) });
}
/** 全字段计划生成器。 */
const planArb: fc.Arbitrary<Plan> = fc.record(
Object.fromEntries(FIELDS.map((f) => [f, fieldPlanArb(f)])) as {
[K in CostField]: fc.Arbitrary<FieldPlan>;
},
);
/** 评分快照生成器(评分已完成:Risk_Score 与 Risk_Grade 均存在)。 */
const scoringArb: fc.Arbitrary<ScoringSnapshot> = fc.record({
riskScore: fc.integer({ min: 0, max: 100 }),
riskGrade: fc.constantFrom<RiskGrade>(...RISK_GRADE_VALUES),
});
// ----------------------------------------------------------------------------
// 属性。
// ----------------------------------------------------------------------------
describe('Property 34: 缺失成本输入兜底为假设 (Req 8.4)', () => {
it('任意缺失成本输入:采用行业默认值且依赖项标注"智能体假设"', () => {
fc.assert(
fc.property(scoringArb, planArb, (scoring, plan) => {
const partial = estimate(scoring, buildInputs(plan));
const fullyProvided = estimate(scoring, buildFullyProvidedInputs(plan));
const partialByName = new Map(
partial.breakdown.map((item) => [item.name, item]),
);
const fullByName = new Map(
fullyProvided.breakdown.map((item) => [item.name, item]),
);
// (1) 采纳行业默认值:缺失场景与"显式补齐为默认值"场景的各拆解项金额逐项相等。
for (const item of partial.breakdown) {
const counterpart = fullByName.get(item.name);
expect(counterpart).toBeDefined();
expect(item.amount).toBe(counterpart!.amount);
}
// (2) 来源标注:每个缺失字段所依赖的拆解项 provenance 必为"智能体假设"。
for (const field of FIELDS) {
if (plan[field].present) {
continue;
}
for (const itemName of DEPENDENT_ITEMS[field]) {
const item = partialByName.get(itemName);
expect(item).toBeDefined();
expect(item!.provenance).toBe(ASSUMED_PROVENANCE);
}
}
}),
{ numRuns: 100 },
);
});
it('反向校验:全部成本输入均提供时无拆解项被标"智能体假设"', () => {
fc.assert(
fc.property(scoringArb, planArb, (scoring, plan) => {
// 强制所有字段均提供。
const allPresent: Plan = Object.fromEntries(
FIELDS.map((f) => [f, { present: true, value: plan[f].value }]),
) as Plan;
const result = estimate(scoring, buildInputs(allPresent));
for (const item of result.breakdown) {
expect(item.provenance).not.toBe(ASSUMED_PROVENANCE);
}
}),
{ numRuns: 100 },
);
});
});
+148
View File
@@ -0,0 +1,148 @@
/**
* 盈利分析引擎单元测试:确定性、口径一致性与各报价模式的关键不变式。
*/
import { describe, expect, it } from 'vitest';
import {
analyzeProfitability,
type ProfitabilityInputs,
} from '../profitability.js';
const perHeadInputs: ProfitabilityInputs = {
businessType: '岗位外包',
region: '上海',
pricingModel: 'per_head',
contractMonths: 12,
positions: [
{ name: '运维工程师', headcount: 10, monthlyGrossSalary: 10000, unitPrice: 16000 },
],
attritionMonthlyRate: 0.03,
recruitingCostPerHire: 3000,
trainingCostPerHead: 2000,
accountPeriodMonths: 2,
riskGrade: '中',
};
describe('analyzeProfitability — 通用', () => {
it('确定性:相同输入产生相同输出', () => {
expect(analyzeProfitability(perHeadInputs)).toEqual(analyzeProfitability(perHeadInputs));
});
it('全口径成本 > 应发工资(加载系数 > 1', () => {
const r = analyzeProfitability(perHeadInputs);
expect(r.loadingFactor).toBeGreaterThan(1);
const pos = r.positions[0]!;
expect(pos.perHead.fullyLoaded).toBeGreaterThan(pos.perHead.grossSalary);
// 拆解项加总等于全口径(两位小数容差)。
const sum =
pos.perHead.grossSalary +
pos.perHead.socialInsurance +
pos.perHead.housingFund +
pos.perHead.benefits +
pos.perHead.employerInsurance +
pos.perHead.recruitingAmortized +
pos.perHead.trainingAmortized +
pos.perHead.managementAllocated;
expect(Math.abs(sum - pos.perHead.fullyLoaded)).toBeLessThan(0.05);
});
it('毛利 = 不含税收入 − 总成本;净利 ≤ 毛利', () => {
const m = analyzeProfitability(perHeadInputs).monthly;
expect(Math.abs(m.grossProfit - (m.revenueNet - m.totalCost))).toBeLessThan(0.05);
expect(m.netProfit).toBeLessThanOrEqual(m.grossProfit + 0.001);
});
it('盈亏平衡单价处净利率≈0', () => {
const r = analyzeProfitability(perHeadInputs);
const be = r.breakeven.unitPrice;
expect(be).toBeDefined();
const atBE = analyzeProfitability({
...perHeadInputs,
positions: [{ ...perHeadInputs.positions[0]!, unitPrice: be! }],
});
expect(Math.abs(atBE.monthly.netMargin)).toBeLessThan(0.02);
});
it('合同期 = 月度 × 月数', () => {
const r = analyzeProfitability(perHeadInputs);
expect(r.contract.totalCost).toBeCloseTo(r.monthly.totalCost * 12, 0);
});
it('敏感性:单价 −5% 使净利率下降', () => {
const r = analyzeProfitability(perHeadInputs);
const row = r.sensitivity.find((s) => s.variable.includes('单价'));
expect(row).toBeDefined();
expect(row!.shockedNetMargin).toBeLessThan(row!.baseNetMargin);
});
});
describe('analyzeProfitability — 报价模式', () => {
it('固定总价:收入取合同总额', () => {
const r = analyzeProfitability({
businessType: '项目制外包',
pricingModel: 'fixed_total',
contractMonths: 6,
contractTotal: 1_200_000,
positions: [{ name: '开发', headcount: 5, monthlyGrossSalary: 20000 }],
});
expect(r.contract.revenueGross).toBeCloseTo(1_200_000, 0);
});
it('成本加成:markup 越高净利越高', () => {
const base: ProfitabilityInputs = {
businessType: '业务/服务外包',
pricingModel: 'cost_plus',
positions: [{ name: '客服', headcount: 20, monthlyGrossSalary: 6000 }],
markupRate: 0.15,
};
const low = analyzeProfitability(base).monthly.netMargin;
const high = analyzeProfitability({ ...base, markupRate: 0.3 }).monthly.netMargin;
expect(high).toBeGreaterThan(low);
});
it('劳务派遣默认差额计税', () => {
const r = analyzeProfitability({
businessType: '劳务派遣',
pricingModel: 'per_head',
positions: [{ name: '派遣工', headcount: 30, monthlyGrossSalary: 5000, unitPrice: 6500 }],
});
expect(r.vatMode).toBe('simplified_diff');
});
it('地域影响社保成本(北京公积金高于广东)', () => {
const mk = (region: string): number =>
analyzeProfitability({
businessType: '岗位外包',
pricingModel: 'per_head',
region,
positions: [{ name: 'x', headcount: 1, monthlyGrossSalary: 10000, unitPrice: 16000 }],
}).positions[0]!.perHead.housingFund;
expect(mk('北京')).toBeGreaterThan(mk('广东'));
});
});
describe('analyzeProfitability — 现金流与定价曲线', () => {
it('有账期时存在垫资峰值,且峰值出现在合同执行期内', () => {
const r = analyzeProfitability(perHeadInputs);
expect(r.cashflow.maxAdvance).toBeGreaterThan(0);
expect(r.cashflow.peakMonth).toBeGreaterThanOrEqual(1);
expect(r.cashflow.points.length).toBe(perHeadInputs.contractMonths! + perHeadInputs.accountPeriodMonths!);
});
it('账期越长,垫资峰值越大', () => {
const a = analyzeProfitability({ ...perHeadInputs, accountPeriodMonths: 1 });
const b = analyzeProfitability({ ...perHeadInputs, accountPeriodMonths: 3 });
expect(b.cashflow.maxAdvance).toBeGreaterThan(a.cashflow.maxAdvance);
});
it('定价曲线随价格系数单调递增(单价越高净利率越高)', () => {
const r = analyzeProfitability(perHeadInputs);
expect(r.marginCurve.length).toBeGreaterThan(0);
for (let i = 1; i < r.marginCurve.length; i += 1) {
expect(r.marginCurve[i]!.netMargin).toBeGreaterThan(r.marginCurve[i - 1]!.netMargin);
}
// priceFactor=1 的点净利率应接近当前测算净利率。
const base = r.marginCurve.find((p) => p.priceFactor === 1)!;
expect(Math.abs(base.netMargin - r.monthly.netMargin)).toBeLessThan(0.0005);
});
});
+60
View File
@@ -0,0 +1,60 @@
/**
* Cost_Engine 行业默认成本输入与费率参数(Req 8.1, 8.2, 8.4)。
*
* 当测算所需的成本输入项缺失时,Cost_Engine 采用本模块定义的行业默认值,并将该
* 输入项的 Data_Provenance 标注为"智能体假设"(Req 8.4)。本模块集中沉淀两类常量:
*
* - 风险溢价加价区间按 Risk_Grade 的百分比分级(Req 8.1):四级区间下界恒不大于
* 上界,且更高分级的区间端点恒不低于更低分级,满足"随分级单调"Property 31)。
* - 各费用项的默认费率/参数(垫资年化利率、账期月数、保险费率、准备金计提比例等),
* 作为缺失输入时的兜底取值与费率来源说明依据(Req 8.2, 8.6)。
*/
import type { Money, RiskGrade } from '../domain/common.js';
/**
* 风险溢价加价区间(百分比)按 Risk_Grade 分级。
*
* 下界/上界均以**百分点**表示(如 3 表示 3%)。区间随分级单调非减:
* 低 ⊆ 中 ⊆ 高 ⊆ 极高 的端点逐级抬升,保证更高分级溢价恒不低于更低分级。
*/
export interface PremiumBand {
/** 区间下界(百分点,≥0)。 */
lowerPct: number;
/** 区间上界(百分点,≥ lowerPct)。 */
upperPct: number;
}
/**
* 四级 Risk_Grade 对应的风险溢价加价区间(百分点)。
*
* 低: 0%3%、中: 3%8%、高: 8%15%、极高: 15%25%。
* 相邻分级端点首尾衔接且逐级非减,满足下界≤上界与跨分级单调(Property 31)。
*/
export const PREMIUM_BANDS: Readonly<Record<RiskGrade, PremiumBand>> = {
: { lowerPct: 0, upperPct: 3 },
: { lowerPct: 3, upperPct: 8 },
: { lowerPct: 8, upperPct: 15 },
: { lowerPct: 15, upperPct: 25 },
};
/** 默认基准报价(元):缺失合同/报价基准金额时的行业兜底取值。 */
export const DEFAULT_BASELINE_QUOTE: Money = 1_000_000;
/** 默认垫资年化利率(小数,6%)。 */
export const DEFAULT_ANNUAL_INTEREST_RATE = 0.06;
/** 默认垫资周期(账期)月数。 */
export const DEFAULT_ADVANCE_MONTHS = 2;
/** 默认保险费率(小数,1%),以投保基数计提。 */
export const DEFAULT_INSURANCE_RATE = 0.01;
/** 默认补偿准备金计提比例(小数,5%),以补偿基数计提。 */
export const DEFAULT_COMPENSATION_RESERVE_RATE = 0.05;
/** 默认坏账准备金计提比例(小数,2%),以坏账基数计提。 */
export const DEFAULT_BAD_DEBT_RATE = 0.02;
/** 一年的月数,用于将年化利率折算到账期月数。 */
export const MONTHS_PER_YEAR = 12;
+28
View File
@@ -0,0 +1,28 @@
/**
* Cost_Engine 费用测算错误类型(Req 8.5)。
*
* 费用测算引擎在评分尚未完成时拒绝执行测算并抛出语义化错误,供上层捕获并向
* 评估者返回"评分未完成"的提示。错误处理遵循统一原则:前置校验、错误可解释、
* 失败不产生无意义的费用测算结果。
*/
/**
* 评分未完成错误(Req 8.5)。
*
* 当本次 Assessment 的风险评分尚未完成(缺少 Risk_Score 或 Risk_Grade)时抛出。
* Cost_Engine 据此拒绝执行费用测算并返回评分未完成的提示。
*/
export class ScoringNotCompleteError extends Error {
/** 面向评估者的可读提示,固定包含"评分未完成"。 */
readonly userMessage: string;
public constructor(
message = '评分未完成:请先完成风险评分与分级后再执行费用测算',
) {
super(message);
this.name = 'ScoringNotCompleteError';
this.userMessage = message;
// 维持 instanceof 在编译目标下正确工作。
Object.setPrototypeOf(this, new.target.prototype);
}
}
+287
View File
@@ -0,0 +1,287 @@
/**
* Cost_Engine 费用测算(Req 8.1-8.6)。
*
* 本模块实现 `estimate`:在风险评分完成后,依据 Risk_Grade 与 Risk_Score 计算可量化的
* 费用/定价测算,输出 {@link CostEstimate}。核心契约:
*
* - 前置:评分已完成(Risk_Score 与 Risk_Grade 均已产生),否则抛
* {@link ScoringNotCompleteError}Req 8.5)。
* - 风险溢价加价区间依 Risk_Grade + Risk_Score 计算:下界恒不大于上界,且随分级单调
* 非减(Req 8.1Property 31)。
* - 垫资利息、保险费用、补偿准备金、坏账准备金各项金额恒非负,且每项标注所依据的
* 输入项与所采用的费率/参数来源(Req 8.2, 8.6Property 32)。
* - 输出基准报价与风险调整后报价(后者恒不低于基准),各项成本拆解之和与报价口径
* 一致(Req 8.3Property 33)。
* - 缺失的成本输入项采用行业默认值并将其 Data_Provenance 标注为"智能体假设"
* Req 8.4Property 34)。
*
* 设计为确定性纯函数:相同输入恒产生相同输出,便于属性化测试约束其正确性。
*/
import type {
CostEstimate,
CostLineItem,
} from '../domain/cost.js';
import type {
DataProvenance,
Money,
RiskGrade,
RiskScore,
} from '../domain/common.js';
import type { Assessment } from '../domain/assessment.js';
import { ASSUMED_PROVENANCE, isAssumption } from '../domain/provenance.js';
import {
DEFAULT_ADVANCE_MONTHS,
DEFAULT_ANNUAL_INTEREST_RATE,
DEFAULT_BAD_DEBT_RATE,
DEFAULT_BASELINE_QUOTE,
DEFAULT_COMPENSATION_RESERVE_RATE,
DEFAULT_INSURANCE_RATE,
MONTHS_PER_YEAR,
PREMIUM_BANDS,
} from './defaults.js';
import { ScoringNotCompleteError } from './errors.js';
/**
* 费用测算的成本输入项(CostInputsReq 8.2-8.4)。
*
* 全部字段均为可选;任一字段缺失时,Cost_Engine 采用对应行业默认值并将该输入项的
* Data_Provenance 标注为"智能体假设"(Req 8.4)。金额单位为人民币元,费率/比例为小数
* (如 0.06 表示 6%)。
*/
export interface CostInputs {
/** 基准报价 / 合同基准金额(元);风险溢价与各准备金的计提基数。 */
baselineQuote?: Money;
/** 垫资本金(元);缺失时默认取基准报价。 */
advancePrincipal?: Money;
/** 垫资周期(账期)月数;用于将年化利率折算到账期。 */
advanceMonths?: number;
/** 垫资年化利率(小数)。 */
annualInterestRate?: number;
/** 投保基数(元);缺失时默认取基准报价。 */
insuredAmount?: Money;
/** 保险费率(小数)。 */
insuranceRate?: number;
/** 经济补偿计提基数(元);缺失时默认取基准报价。 */
compensationBase?: Money;
/** 补偿准备金计提比例(小数)。 */
compensationReserveRate?: number;
/** 坏账计提基数(元);缺失时默认取基准报价。 */
badDebtBase?: Money;
/** 坏账准备金计提比例(小数)。 */
badDebtRate?: number;
}
/**
* 评分快照:费用测算所需的评分结果子集。
*
* 以 {@link Assessment} 的 `riskScore` 与 `riskGrade` 子集为入参,使本函数既可直接接收
* 完整 Assessment,也可接收最小评分对象,且与评估记录的其他字段解耦。
*/
export type ScoringSnapshot = Pick<Assessment, 'riskScore' | 'riskGrade'>;
/** 单个解析后的数值输入:携带最终取值与其数据来源标注。 */
interface ResolvedInput {
/** 最终采用的数值(用户输入或行业默认值)。 */
value: number;
/** 数据来源:缺失而采用默认值时为"智能体假设",否则为"用户输入"。 */
provenance: DataProvenance;
}
/**
* 解析一个可选数值输入:提供则标"用户输入",缺失则采用默认值并标"智能体假设"Req 8.4)。
*
* @param provided 调用方提供的输入值(可能为 undefined)。
* @param fallback 缺失时采用的行业默认值。
* @returns 解析后的取值与来源标注。
*/
function resolveInput(provided: number | undefined, fallback: number): ResolvedInput {
if (provided === undefined) {
return { value: fallback, provenance: ASSUMED_PROVENANCE };
}
return { value: provided, provenance: '用户输入' };
}
/**
* 合并多个来源标注:任一为"智能体假设"则结果为"智能体假设",否则为"用户输入"Req 8.4)。
*/
function combineProvenance(inputs: ResolvedInput[]): DataProvenance {
return inputs.some((input) => isAssumption(input.provenance))
? ASSUMED_PROVENANCE
: '用户输入';
}
/** 将金额规整为非负、保留两位小数(抵消二进制浮点误差)。 */
function roundMoney(value: number): Money {
const factor = 100;
const rounded = Math.round((value + Number.EPSILON) * factor) / factor;
// `+ 0` 将可能出现的负零规范化为正零,确保金额恒为非负。
return Math.max(0, rounded) + 0;
}
/** 将来源标注转为人类可读的来源短语,用于费率/参数来源说明(Req 8.2, 8.6)。 */
function sourceLabel(provenance: DataProvenance): string {
return isAssumption(provenance) ? '行业默认值(智能体假设)' : '用户输入';
}
/**
* 计算风险溢价加价区间(百分比)并依 Risk_Score 在区间内定位实际加价率(Req 8.1)。
*
* 区间取自 {@link PREMIUM_BANDS}[grade],随分级单调;实际加价率在区间内按 Risk_Score
* 线性定位(score 0→下界、100→上界),用于风险调整后报价的溢价计算。
*
* @param grade Risk_Grade。
* @param score Risk_Score0 至 100)。
* @returns 区间端点(百分点)与折算后的实际加价率(小数)。
*/
function computePremium(
grade: RiskGrade,
score: RiskScore,
): { lowerPct: number; upperPct: number; appliedRate: number } {
const band = PREMIUM_BANDS[grade];
const position = Math.min(1, Math.max(0, score / 100));
const appliedPct = band.lowerPct + (band.upperPct - band.lowerPct) * position;
return {
lowerPct: band.lowerPct,
upperPct: band.upperPct,
appliedRate: appliedPct / 100,
};
}
/**
* 执行费用与定价量化测算(Req 8.1-8.6)。
*
* @param scoring 评分快照(完整 Assessment 或其 riskScore/riskGrade 子集)。
* @param costInputs 成本输入项;任一字段缺失则采用行业默认值并标"智能体假设"Req 8.4)。
* @returns 费用测算结果 {@link CostEstimate}。
* @throws {ScoringNotCompleteError} 当评分尚未完成(缺少 Risk_Score 或 Risk_Grade)时(Req 8.5)。
*/
export function estimate(
scoring: ScoringSnapshot,
costInputs: CostInputs = {},
): CostEstimate {
// 前置:评分必须已完成(Req 8.5)。
if (scoring.riskScore === undefined || scoring.riskGrade === undefined) {
throw new ScoringNotCompleteError();
}
const riskScore: RiskScore = scoring.riskScore;
const riskGrade: RiskGrade = scoring.riskGrade;
// 解析基准报价(其他基数缺省时回退至基准报价)。
const baseline = resolveInput(costInputs.baselineQuote, DEFAULT_BASELINE_QUOTE);
// 解析各费用项的基数与费率/参数(缺失项采用行业默认值并标"智能体假设")。
const advancePrincipal = resolveInput(costInputs.advancePrincipal, baseline.value);
const advanceMonths = resolveInput(costInputs.advanceMonths, DEFAULT_ADVANCE_MONTHS);
const annualInterestRate = resolveInput(
costInputs.annualInterestRate,
DEFAULT_ANNUAL_INTEREST_RATE,
);
const insuredAmount = resolveInput(costInputs.insuredAmount, baseline.value);
const insuranceRate = resolveInput(costInputs.insuranceRate, DEFAULT_INSURANCE_RATE);
const compensationBase = resolveInput(costInputs.compensationBase, baseline.value);
const compensationReserveRate = resolveInput(
costInputs.compensationReserveRate,
DEFAULT_COMPENSATION_RESERVE_RATE,
);
const badDebtBase = resolveInput(costInputs.badDebtBase, baseline.value);
const badDebtRate = resolveInput(costInputs.badDebtRate, DEFAULT_BAD_DEBT_RATE);
// 风险溢价加价区间(依 Risk_Grade + Risk_ScoreReq 8.1)。
const premium = computePremium(riskGrade, riskScore);
// 各费用项金额(恒非负,Req 8.2)。
const baselineQuote = roundMoney(baseline.value);
const riskPremiumAmount = roundMoney(baseline.value * premium.appliedRate);
const advanceInterest = roundMoney(
advancePrincipal.value * annualInterestRate.value * (advanceMonths.value / MONTHS_PER_YEAR),
);
const insuranceCost = roundMoney(insuredAmount.value * insuranceRate.value);
const compensationReserve = roundMoney(
compensationBase.value * compensationReserveRate.value,
);
const badDebtReserve = roundMoney(badDebtBase.value * badDebtRate.value);
// 风险调整后报价 = 基准报价 + 风险溢价 + 各项成本(恒不低于基准,Req 8.3)。
const riskAdjustedQuote = roundMoney(
baselineQuote +
riskPremiumAmount +
advanceInterest +
insuranceCost +
compensationReserve +
badDebtReserve,
);
// 费用拆解:基准报价 + 各加价/成本项,其金额之和与风险调整后报价口径一致(Req 8.3)。
const breakdown: CostLineItem[] = [
{
name: '基准报价',
amount: baselineQuote,
basisInputs: ['基准报价/合同基准金额'],
rateSource: `基准报价取值来源:${sourceLabel(baseline.provenance)}`,
provenance: baseline.provenance,
},
{
name: '风险溢价',
amount: riskPremiumAmount,
basisInputs: ['基准报价/合同基准金额', 'Risk_Grade', 'Risk_Score'],
rateSource:
`按 Risk_Grade=${riskGrade} 的溢价区间 ` +
`${premium.lowerPct}%~${premium.upperPct}% 内依 Risk_Score=${riskScore} ` +
`线性定位,实际加价率 ${(premium.appliedRate * 100).toFixed(2)}%`,
provenance: baseline.provenance,
},
{
name: '垫资利息',
amount: advanceInterest,
basisInputs: ['垫资本金', '垫资周期(月)', '年化利率'],
rateSource:
`年化垫资利率 ${(annualInterestRate.value * 100).toFixed(2)}%` +
`(${sourceLabel(annualInterestRate.provenance)})` +
`账期 ${advanceMonths.value} 个月(${sourceLabel(advanceMonths.provenance)})`,
provenance: combineProvenance([advancePrincipal, advanceMonths, annualInterestRate]),
},
{
name: '保险费用',
amount: insuranceCost,
basisInputs: ['投保基数', '保险费率'],
rateSource:
`保险费率 ${(insuranceRate.value * 100).toFixed(2)}%` +
`(${sourceLabel(insuranceRate.provenance)})`,
provenance: combineProvenance([insuredAmount, insuranceRate]),
},
{
name: '补偿准备金',
amount: compensationReserve,
basisInputs: ['经济补偿计提基数', '补偿准备金计提比例'],
rateSource:
`补偿准备金计提比例 ${(compensationReserveRate.value * 100).toFixed(2)}%` +
`(${sourceLabel(compensationReserveRate.provenance)})`,
provenance: combineProvenance([compensationBase, compensationReserveRate]),
},
{
name: '坏账准备金',
amount: badDebtReserve,
basisInputs: ['坏账计提基数', '坏账准备金计提比例'],
rateSource:
`坏账准备金计提比例 ${(badDebtRate.value * 100).toFixed(2)}%` +
`(${sourceLabel(badDebtRate.provenance)})`,
provenance: combineProvenance([badDebtBase, badDebtRate]),
},
];
return {
riskPremiumRange: {
lower: premium.lowerPct,
upper: premium.upperPct,
unit: '百分比',
},
advanceInterest,
insuranceCost,
compensationReserve,
badDebtReserve,
baselineQuote,
riskAdjustedQuote,
breakdown,
};
}
+11
View File
@@ -0,0 +1,11 @@
/**
* Cost_Engine 费用测算引擎模块聚合导出(Req 8)。
*
* 提供 `estimate` 费用测算入口、成本输入类型、行业默认常量与评分未完成错误。
*/
export * from './estimate.js';
export * from './defaults.js';
export * from './errors.js';
export * from './rates.js';
export * from './profitability.js';
+595
View File
@@ -0,0 +1,595 @@
/**
* Cost_Engine 盈利分析引擎(确定性纯函数)。
*
* 按业务类型与报价模式建立「收入(销售结算)账」与「全口径成本账」,产出毛利/净利/
* 盈亏平衡/敏感性,并联动风险评分计提风险准备金。税务做价外增值税 + 附加的近似估算。
*
* 口径与费率为行业近似默认,缺失项采用默认并在 `assumptions` 标注("智能体假设"),
* 须经财务复核。本模块不实现任何 UI/IO,便于属性化与单元测试约束其确定性与一致性。
*
* 首批支持的报价模式:
* - per_head 人头费率制(岗位外包/劳务派遣)
* - cost_plus 成本加成 / 人月单价(业务/服务外包 T&M)
* - fixed_total 固定总价(项目制外包)
* - volume 按量/产能(BPO,近似以人月当量计)
*/
import type { RiskGrade } from '../domain/common.js';
import {
getRegionRates,
mergeRates,
totalSocialInsuranceRate,
type RegionRates,
} from './rates.js';
/** 报价 / 销售结算模式。 */
export type PricingModel = 'per_head' | 'cost_plus' | 'fixed_total' | 'volume';
/** 增值税计税方式。 */
export type VatMode = 'general' | 'simplified_diff';
/** 单个岗位(成本与产能的最小单元)。 */
export interface PositionInput {
/** 岗位名称。 */
name: string;
/** 人数。 */
headcount: number;
/** 应发工资(元/人/月)。 */
monthlyGrossSalary: number;
/** 社保缴费基数(元/人/月);缺省取应发工资。 */
socialInsuranceBase?: number;
/** 公积金缴费基数(元/人/月);缺省取应发工资。 */
housingFundBase?: number;
/** 福利/补贴(元/人/月)。 */
monthlyBenefits?: number;
/**
* 对客单价(元/人/月)。per_head/volume 为对客月单价;cost_plus 为人月单价。
* per_head 缺省时可由 managementFeePerHeadMonth 推导(全成本 + 管理费)。
*/
unitPrice?: number;
}
/** 盈利分析输入。 */
export interface ProfitabilityInputs {
/** 业务类型(决定默认计税方式等)。 */
businessType: string;
/** 适用地域(决定社保/公积金/税率默认)。 */
region?: string;
/** 报价模式。 */
pricingModel: PricingModel;
/** 合同周期(月),默认 12。 */
contractMonths?: number;
/** 岗位明细(≥1)。 */
positions: readonly PositionInput[];
/** 派遣管理费制:管理费(元/人/月),用于 per_head 缺单价时推导对客单价。 */
managementFeePerHeadMonth?: number;
/** 成本加成率(小数),cost_plus 模式。 */
markupRate?: number;
/** 固定合同总额(含税,元),fixed_total 模式。 */
contractTotal?: number;
/** 月流失率(小数),影响招聘/培训摊销与平均在职月数。 */
attritionMonthlyRate?: number;
/** 单次招聘成本(元/人)。 */
recruitingCostPerHire?: number;
/** 人均培训成本(元/人)。 */
trainingCostPerHead?: number;
/** 雇主责任/工伤补充险费率(按应发工资计,小数),默认 0.01。 */
employerLiabilityInsuranceRate?: number;
/** 管理跨度(1 名管理人员管多少人)。 */
managementSpan?: number;
/** 管理人员全成本(元/人/月)。 */
managementMonthlyCostPerManager?: number;
/** 账期(月),用于垫资测算。 */
accountPeriodMonths?: number;
/** 年化利率(小数),默认 0.06。 */
annualInterestRate?: number;
/** 期间费用率(占收入,小数),默认 0.05。 */
periodExpenseRate?: number;
/** 有效利用率(小数,BPO/服务外包产能),默认 1。 */
utilizationRate?: number;
/** 计税方式;缺省按业务类型(劳务派遣→差额,其余→一般)。 */
vatMode?: VatMode;
/** 风险分级(联动风险准备金计提)。 */
riskGrade?: RiskGrade;
/** 坏账准备金计提比例(小数,按客户信用等级注入);缺省 0。 */
badDebtRate?: number;
/** 费率覆盖(项目实际手填覆盖地域默认)。 */
rateOverrides?: Partial<RegionRates>;
}
/** 单岗位全口径人月成本拆解(元/人/月)。 */
export interface PositionCostBreakdown {
grossSalary: number;
socialInsurance: number;
housingFund: number;
benefits: number;
employerInsurance: number;
recruitingAmortized: number;
trainingAmortized: number;
managementAllocated: number;
/** 全口径人月成本合计。 */
fullyLoaded: number;
}
/** 单岗位汇总。 */
export interface PositionResult {
name: string;
headcount: number;
perHead: PositionCostBreakdown;
/** 全成本加载系数 = 全口径/应发。 */
loadingFactor: number;
monthlyCost: number;
monthlyRevenue: number;
}
/** 盈亏平衡。 */
export interface Breakeven {
/** 盈亏平衡对客单价(per_head/volume,元/人/月)。 */
unitPrice?: number;
/** 盈亏平衡加成率(cost_plus)。 */
markupRate?: number;
/** 盈亏平衡利用率(cost_plus/volume,小数)。 */
utilization?: number;
}
/** 敏感性单行。 */
export interface SensitivityRow {
variable: string;
baseNetMargin: number;
shockedNetMargin: number;
deltaPct: number;
}
/** 现金流累计点(月度)。 */
export interface CashflowPoint {
/** 第几个月(含账期滞后)。 */
month: number;
/** 累计净现金(流入−流出)。 */
cumulative: number;
}
/** 现金流与垫资峰值(外包先垫付成本、账期后收款)。 */
export interface Cashflow {
/** 峰值垫资需求(合同期内累计净现金的最大负值绝对数)。 */
maxAdvance: number;
/** 峰值出现的月份。 */
peakMonth: number;
/** 累计现金流曲线。 */
points: CashflowPoint[];
}
/** 盈亏平衡 / 定价空间曲线单点。 */
export interface MarginCurvePoint {
/** 相对当前报价的价格系数(如 0.95 = −5%)。 */
priceFactor: number;
/** 对应对客单价(首个岗位口径,无单价概念时为 null)。 */
unitPrice: number | null;
/** 该价格下的月净利率。 */
netMargin: number;
}
/** 盈利分析结果。 */
export interface ProfitabilityResult {
businessType: string;
pricingModel: PricingModel;
region: string;
contractMonths: number;
vatMode: VatMode;
positions: PositionResult[];
totalHeadcount: number;
/** 加权全成本加载系数。 */
loadingFactor: number;
/** 月度账(不含税口径的收入与利润)。 */
monthly: {
revenueGross: number;
revenueNet: number;
laborCost: number;
managementCost: number;
totalCost: number;
grossProfit: number;
grossMargin: number;
periodExpense: number;
financeCost: number;
vat: number;
surcharge: number;
riskReserve: number;
badDebtReserve: number;
netProfit: number;
netMargin: number;
};
/** 合同期账。 */
contract: {
revenueGross: number;
revenueNet: number;
totalCost: number;
grossProfit: number;
netProfit: number;
};
breakeven: Breakeven;
sensitivity: SensitivityRow[];
/** 现金流与垫资峰值。 */
cashflow: Cashflow;
/** 盈亏平衡 / 定价空间曲线(价格系数 → 净利率)。 */
marginCurve: MarginCurvePoint[];
/** 采用了默认值(智能体假设)的说明。 */
assumptions: string[];
}
/* ------------------------------------------------------------------ *
* 常量与工具
* ------------------------------------------------------------------ */
const DEFAULT_CONTRACT_MONTHS = 12;
const DEFAULT_INSURANCE_RATE = 0.01;
const DEFAULT_ANNUAL_INTEREST = 0.06;
const DEFAULT_PERIOD_EXPENSE_RATE = 0.05;
const MONTHS_PER_YEAR = 12;
/** 风险分级 → 风险准备金计提比例(占收入,小数)。 */
const RESERVE_RATE_BY_GRADE: Record<RiskGrade, number> = {
: 0,
: 0.02,
: 0.05,
极高: 0.08,
};
function round2(v: number): number {
return Math.round((v + Number.EPSILON) * 100) / 100;
}
function defaultVatMode(businessType: string): VatMode {
return businessType === '劳务派遣' ? 'simplified_diff' : 'general';
}
/* ------------------------------------------------------------------ *
* 核心计算(不含敏感性,供敏感性扰动复用以避免递归)
* ------------------------------------------------------------------ */
interface CoreOutput {
result: Omit<ProfitabilityResult, 'sensitivity' | 'marginCurve'>;
}
function computeCore(inputs: ProfitabilityInputs): CoreOutput {
const assumptions: string[] = [];
const note = (cond: boolean, msg: string): void => {
if (cond) assumptions.push(msg);
};
const contractMonths = inputs.contractMonths ?? DEFAULT_CONTRACT_MONTHS;
note(inputs.contractMonths === undefined, `合同周期默认 ${DEFAULT_CONTRACT_MONTHS} 个月`);
const baseRates = getRegionRates(inputs.region);
const rates = mergeRates(baseRates, inputs.rateOverrides);
const socialRate = totalSocialInsuranceRate(rates.socialInsurance);
const insuranceRate = inputs.employerLiabilityInsuranceRate ?? DEFAULT_INSURANCE_RATE;
note(inputs.employerLiabilityInsuranceRate === undefined, `雇主责任险费率默认 ${(DEFAULT_INSURANCE_RATE * 100).toFixed(1)}%`);
const attrition = inputs.attritionMonthlyRate ?? 0;
const avgTenureMonths = attrition > 0 ? 1 / attrition : contractMonths;
const recruiting = inputs.recruitingCostPerHire ?? 0;
const training = inputs.trainingCostPerHead ?? 0;
const mgmtSpan = inputs.managementSpan ?? 0;
const mgmtCostPerMgr = inputs.managementMonthlyCostPerManager ?? 0;
const mgmtPerHead = mgmtSpan > 0 ? mgmtCostPerMgr / mgmtSpan : 0;
const utilization = inputs.utilizationRate ?? 1;
const periodRate = inputs.periodExpenseRate ?? DEFAULT_PERIOD_EXPENSE_RATE;
note(inputs.periodExpenseRate === undefined, `期间费用率默认 ${(DEFAULT_PERIOD_EXPENSE_RATE * 100).toFixed(0)}%`);
const annualInterest = inputs.annualInterestRate ?? DEFAULT_ANNUAL_INTEREST;
const accountMonths = inputs.accountPeriodMonths ?? 0;
const vatMode = inputs.vatMode ?? defaultVatMode(inputs.businessType);
const vatRate = vatMode === 'simplified_diff' ? rates.vatSimplifiedRate : rates.vatGeneralRate;
// —— 逐岗位全口径成本 ——
const positions: PositionResult[] = [];
let monthlyLaborCost = 0;
let monthlyMgmtCost = 0;
let monthlyPassthrough = 0; // 差额计税口径:工资+社保+公积金(代缴部分)
let totalHeadcount = 0;
let weightedLoadingNum = 0;
let weightedLoadingDen = 0;
for (const p of inputs.positions) {
const gross = p.monthlyGrossSalary;
const socialBase = p.socialInsuranceBase ?? gross;
const housingBase = p.housingFundBase ?? gross;
const benefits = p.monthlyBenefits ?? 0;
const social = socialBase * socialRate;
const housing = housingBase * rates.housingFund;
const employerInsurance = gross * insuranceRate;
const recruitingAmort = avgTenureMonths > 0 ? recruiting / avgTenureMonths : 0;
const trainingAmort = avgTenureMonths > 0 ? training / avgTenureMonths : 0;
const fullyLoaded =
gross + social + housing + benefits + employerInsurance + recruitingAmort + trainingAmort + mgmtPerHead;
const perHead: PositionCostBreakdown = {
grossSalary: round2(gross),
socialInsurance: round2(social),
housingFund: round2(housing),
benefits: round2(benefits),
employerInsurance: round2(employerInsurance),
recruitingAmortized: round2(recruitingAmort),
trainingAmortized: round2(trainingAmort),
managementAllocated: round2(mgmtPerHead),
fullyLoaded: round2(fullyLoaded),
};
const monthlyCost = fullyLoaded * p.headcount;
// —— 收入(按报价模式,单岗位口径仅 per_head/volume/cost_plus(人月单价) 有意义)——
const billableUnits = p.headcount * (inputs.pricingModel === 'per_head' ? 1 : utilization);
let monthlyRevenue = 0;
if (inputs.pricingModel === 'per_head' || inputs.pricingModel === 'volume') {
const unit =
p.unitPrice ??
(inputs.managementFeePerHeadMonth !== undefined
? fullyLoaded + inputs.managementFeePerHeadMonth
: 0);
monthlyRevenue = unit * billableUnits;
} else if (inputs.pricingModel === 'cost_plus') {
monthlyRevenue =
inputs.markupRate !== undefined
? monthlyCost * (1 + inputs.markupRate)
: (p.unitPrice ?? 0) * billableUnits;
}
positions.push({
name: p.name,
headcount: p.headcount,
perHead,
loadingFactor: gross > 0 ? round2(fullyLoaded / gross) : 0,
monthlyCost: round2(monthlyCost),
monthlyRevenue: round2(monthlyRevenue),
});
monthlyLaborCost += (fullyLoaded - mgmtPerHead) * p.headcount;
monthlyMgmtCost += mgmtPerHead * p.headcount;
monthlyPassthrough += (gross + social + housing) * p.headcount;
totalHeadcount += p.headcount;
weightedLoadingNum += fullyLoaded * p.headcount;
weightedLoadingDen += gross * p.headcount;
}
const totalMonthlyCost = monthlyLaborCost + monthlyMgmtCost;
// —— 收入汇总(含税) ——
let revenueGrossMonthly: number;
if (inputs.pricingModel === 'fixed_total') {
const total = inputs.contractTotal ?? 0;
note(inputs.contractTotal === undefined, '固定总价缺失,收入按 0 计');
revenueGrossMonthly = contractMonths > 0 ? total / contractMonths : 0;
} else {
revenueGrossMonthly = positions.reduce((s, p) => s + p.monthlyRevenue, 0);
}
// —— 增值税及附加 ——
let vat: number;
if (vatMode === 'simplified_diff') {
const taxBase = Math.max(0, revenueGrossMonthly - monthlyPassthrough);
vat = (taxBase / (1 + vatRate)) * vatRate;
} else {
vat = (revenueGrossMonthly / (1 + vatRate)) * vatRate;
}
const surcharge = vat * rates.surchargeRate;
const revenueNetMonthly = revenueGrossMonthly - (vatMode === 'general' ? vat : 0);
// —— 毛利 / 净利 ——
const grossProfit = revenueNetMonthly - totalMonthlyCost;
const periodExpense = revenueNetMonthly * periodRate;
const advancePrincipal = totalMonthlyCost * accountMonths;
const financeCost = (advancePrincipal * annualInterest) / MONTHS_PER_YEAR;
const reserveRate = inputs.riskGrade !== undefined ? RESERVE_RATE_BY_GRADE[inputs.riskGrade] : 0;
const riskReserve = revenueNetMonthly * reserveRate;
const badDebtRate = inputs.badDebtRate ?? 0;
const badDebtReserve = revenueNetMonthly * badDebtRate;
note(badDebtRate > 0, `坏账准备金按 ${(badDebtRate * 100).toFixed(1)}%(依客户信用等级)计提`);
const netProfit = grossProfit - periodExpense - financeCost - surcharge - riskReserve - badDebtReserve;
const grossMargin = revenueNetMonthly > 0 ? grossProfit / revenueNetMonthly : 0;
const netMargin = revenueNetMonthly > 0 ? netProfit / revenueNetMonthly : 0;
// —— 盈亏平衡 ——
const breakeven: Breakeven = {};
if (inputs.pricingModel === 'per_head' || inputs.pricingModel === 'volume') {
// 求使单岗位净利=0 的对客单价(以全体加权成本为基础)。
const denom = 1 - periodRate - reserveRate - badDebtRate - (vatMode === 'general' ? vatRate / (1 + vatRate) : 0) * rates.surchargeRate;
const costPerHeadMonth = totalHeadcount > 0 ? totalMonthlyCost / totalHeadcount : 0;
const financePerHead = totalHeadcount > 0 ? financeCost / totalHeadcount : 0;
if (denom > 0) {
const revNetBE = (costPerHeadMonth + financePerHead) / denom;
breakeven.unitPrice = round2(vatMode === 'general' ? revNetBE * (1 + vatRate) : revNetBE);
}
} else if (inputs.pricingModel === 'cost_plus') {
// 求使净利=0 的加成率(近似:覆盖期间费用+财务+附加+准备金)。
const overheadRate = periodRate + reserveRate + badDebtRate;
breakeven.markupRate = round2(overheadRate / Math.max(0.0001, 1 - overheadRate));
}
// 现金流与垫资峰值:成本按月流出,收款滞后账期月数流入;累计净现金的最低点即峰值垫资需求。
const horizon = contractMonths + accountMonths;
const cashPoints: CashflowPoint[] = [];
let cum = 0;
let maxAdvance = 0;
let peakMonth = 0;
for (let t = 1; t <= horizon; t += 1) {
const outflow = t <= contractMonths ? totalMonthlyCost : 0;
const serviceMonth = t - accountMonths;
const inflow = serviceMonth >= 1 && serviceMonth <= contractMonths ? revenueGrossMonthly : 0;
cum += inflow - outflow;
cashPoints.push({ month: t, cumulative: round2(cum) });
if (cum < -maxAdvance) {
maxAdvance = -cum;
peakMonth = t;
}
}
const cashflow: Cashflow = { maxAdvance: round2(maxAdvance), peakMonth, points: cashPoints };
const result: Omit<ProfitabilityResult, 'sensitivity' | 'marginCurve'> = {
businessType: inputs.businessType,
pricingModel: inputs.pricingModel,
region: rates.regionName,
contractMonths,
vatMode,
positions,
totalHeadcount,
loadingFactor: weightedLoadingDen > 0 ? round2(weightedLoadingNum / weightedLoadingDen) : 0,
monthly: {
revenueGross: round2(revenueGrossMonthly),
revenueNet: round2(revenueNetMonthly),
laborCost: round2(monthlyLaborCost),
managementCost: round2(monthlyMgmtCost),
totalCost: round2(totalMonthlyCost),
grossProfit: round2(grossProfit),
grossMargin: round2(grossMargin * 10000) / 10000,
periodExpense: round2(periodExpense),
financeCost: round2(financeCost),
vat: round2(vat),
surcharge: round2(surcharge),
riskReserve: round2(riskReserve),
badDebtReserve: round2(badDebtReserve),
netProfit: round2(netProfit),
netMargin: round2(netMargin * 10000) / 10000,
},
contract: {
revenueGross: round2(revenueGrossMonthly * contractMonths),
revenueNet: round2(revenueNetMonthly * contractMonths),
totalCost: round2(totalMonthlyCost * contractMonths),
grossProfit: round2(grossProfit * contractMonths),
netProfit: round2(netProfit * contractMonths),
},
breakeven,
cashflow,
assumptions,
};
return { result };
}
/* ------------------------------------------------------------------ *
* 敏感性
* ------------------------------------------------------------------ */
function netMarginOf(inputs: ProfitabilityInputs): number {
return computeCore(inputs).result.monthly.netMargin;
}
function buildSensitivity(inputs: ProfitabilityInputs, baseNetMargin: number): SensitivityRow[] {
const rows: SensitivityRow[] = [];
const add = (variable: string, shocked: ProfitabilityInputs): void => {
const m = netMarginOf(shocked);
rows.push({
variable,
baseNetMargin: Math.round(baseNetMargin * 1000) / 1000,
shockedNetMargin: Math.round(m * 1000) / 1000,
deltaPct: Math.round((m - baseNetMargin) * 1000) / 10, // 百分点,1 位小数
});
};
// 单价 5%
add('对客单价 5%', {
...inputs,
positions: inputs.positions.map((p) => ({
...p,
...(p.unitPrice !== undefined ? { unitPrice: p.unitPrice * 0.95 } : {}),
})),
...(inputs.contractTotal !== undefined ? { contractTotal: inputs.contractTotal * 0.95 } : {}),
...(inputs.managementFeePerHeadMonth !== undefined
? { managementFeePerHeadMonth: inputs.managementFeePerHeadMonth * 0.95 }
: {}),
...(inputs.markupRate !== undefined ? { markupRate: inputs.markupRate - 0.05 } : {}),
});
// 社保基数 +10%
add('社保基数 +10%', {
...inputs,
positions: inputs.positions.map((p) => ({
...p,
socialInsuranceBase: (p.socialInsuranceBase ?? p.monthlyGrossSalary) * 1.1,
})),
});
// 月流失率 +5 个百分点
add('月流失率 +5 个百分点', {
...inputs,
attritionMonthlyRate: (inputs.attritionMonthlyRate ?? 0) + 0.05,
});
// 账期 +1 月
add('账期 +1 个月', {
...inputs,
accountPeriodMonths: (inputs.accountPeriodMonths ?? 0) + 1,
});
// 利用率 10 个百分点
add('利用率 10 个百分点', {
...inputs,
utilizationRate: Math.max(0, (inputs.utilizationRate ?? 1) - 0.1),
});
return rows;
}
/* ------------------------------------------------------------------ *
* 盈亏平衡 / 定价空间曲线
* ------------------------------------------------------------------ */
/** 按价格系数缩放收入相关输入(单价/加成/合同总额/管理费)。 */
function scalePrice(inputs: ProfitabilityInputs, factor: number): ProfitabilityInputs {
return {
...inputs,
positions: inputs.positions.map((p) => ({
...p,
...(p.unitPrice !== undefined ? { unitPrice: p.unitPrice * factor } : {}),
})),
...(inputs.contractTotal !== undefined ? { contractTotal: inputs.contractTotal * factor } : {}),
...(inputs.managementFeePerHeadMonth !== undefined
? { managementFeePerHeadMonth: inputs.managementFeePerHeadMonth * factor }
: {}),
...(inputs.markupRate !== undefined ? { markupRate: (1 + inputs.markupRate) * factor - 1 } : {}),
};
}
/** 构建价格系数 → 净利率曲线(用于盈亏平衡与定价空间可视化)。 */
function buildMarginCurve(inputs: ProfitabilityInputs): MarginCurvePoint[] {
const factors = [0.85, 0.9, 0.95, 1, 1.05, 1.1, 1.15, 1.2];
const baseUnit = inputs.positions[0]?.unitPrice;
return factors.map((f) => ({
priceFactor: f,
unitPrice: baseUnit !== undefined ? round2(baseUnit * f) : null,
netMargin: Math.round(netMarginOf(scalePrice(inputs, f)) * 10000) / 10000,
}));
}
/* ------------------------------------------------------------------ *
* 公开入口
* ------------------------------------------------------------------ */
/**
* 盈利分析:收入(销售结算)+ 全口径成本 → 毛利/净利/盈亏平衡/敏感性/现金流 + 风险联动。
*
* @param inputs 盈利分析输入。
* @returns 盈利分析结果(含敏感性、现金流与定价空间曲线)。
*/
export function analyzeProfitability(inputs: ProfitabilityInputs): ProfitabilityResult {
const { result } = computeCore(inputs);
const sensitivity = buildSensitivity(inputs, result.monthly.netMargin);
const marginCurve = buildMarginCurve(inputs);
return { ...result, sensitivity, marginCurve };
}
+110
View File
@@ -0,0 +1,110 @@
/**
* 分地域费率表:社保(单位部分各险种)、公积金、增值税及附加(Cost_Engine 盈利分析)。
*
* 设计:内置一套「全国近似默认(CN)」+ 若干地域覆盖(上海/北京/广东),并预留 `rateOverrides`
* 让调用方按项目实际手填覆盖。所有费率为**行业近似默认值**,随地区与年度变化,**须经财务复核**,
* 系统据此标注"智能体假设"。取数 {@link getRegionRates} 按地域回退到全国默认。
*/
/** 社保单位部分各险种费率(小数)。 */
export interface SocialInsuranceRates {
/** 养老(单位)。 */
pension: number;
/** 医疗(单位,含生育并轨地区可合并)。 */
medical: number;
/** 失业(单位)。 */
unemployment: number;
/** 工伤(单位,按行业风险浮动,取基准)。 */
injury: number;
/** 生育(单位;与医疗并轨地区填 0)。 */
maternity: number;
}
/** 某地域的完整费率参数。 */
export interface RegionRates {
/** 地域名(展示用)。 */
regionName: string;
/** 社保单位部分各险种费率。 */
socialInsurance: SocialInsuranceRates;
/** 公积金单位部分比例(小数)。 */
housingFund: number;
/** 增值税一般计税税率(现代服务业,小数)。 */
vatGeneralRate: number;
/** 增值税简易/差额计税征收率(如劳务派遣差额,小数)。 */
vatSimplifiedRate: number;
/** 附加税费合计(占增值税额的比例:城建+教育附加+地方教育附加,小数)。 */
surchargeRate: number;
}
/** 社保单位部分合计费率。 */
export function totalSocialInsuranceRate(s: SocialInsuranceRates): number {
return s.pension + s.medical + s.unemployment + s.injury + s.maternity;
}
/** 全国近似默认费率(CN)。须经财务按属地与年度复核。 */
export const NATIONAL_DEFAULT_RATES: RegionRates = {
regionName: '全国(默认)',
socialInsurance: {
pension: 0.16,
medical: 0.095,
unemployment: 0.005,
injury: 0.004,
maternity: 0.01,
},
housingFund: 0.07,
vatGeneralRate: 0.06,
vatSimplifiedRate: 0.05,
surchargeRate: 0.12,
};
/** 地域费率覆盖表(仅列与全国默认有差异的地域;其余回退全国默认)。 */
export const REGION_RATES: Readonly<Record<string, RegionRates>> = {
CN: NATIONAL_DEFAULT_RATES,
: {
regionName: '上海',
socialInsurance: { pension: 0.16, medical: 0.1, unemployment: 0.005, injury: 0.0026, maternity: 0.01 },
housingFund: 0.07,
vatGeneralRate: 0.06,
vatSimplifiedRate: 0.05,
surchargeRate: 0.12,
},
: {
regionName: '北京',
socialInsurance: { pension: 0.16, medical: 0.09, unemployment: 0.005, injury: 0.002, maternity: 0.008 },
housingFund: 0.12,
vatGeneralRate: 0.06,
vatSimplifiedRate: 0.05,
surchargeRate: 0.12,
},
广: {
regionName: '广东',
socialInsurance: { pension: 0.14, medical: 0.045, unemployment: 0.008, injury: 0.0016, maternity: 0.01 },
housingFund: 0.05,
vatGeneralRate: 0.06,
vatSimplifiedRate: 0.05,
surchargeRate: 0.12,
},
};
/**
* 按地域取费率;未知地域回退全国默认。地域名做包含匹配(如"上海市"命中"上海")。
*/
export function getRegionRates(region: string | undefined): RegionRates {
if (region === undefined || region.trim() === '') {
return NATIONAL_DEFAULT_RATES;
}
const key = Object.keys(REGION_RATES).find((k) => k !== 'CN' && region.includes(k));
return key !== undefined ? REGION_RATES[key] ?? NATIONAL_DEFAULT_RATES : NATIONAL_DEFAULT_RATES;
}
/** 以覆盖项合并费率(用于项目实际手填覆盖默认)。 */
export function mergeRates(base: RegionRates, override?: Partial<RegionRates>): RegionRates {
if (override === undefined) {
return base;
}
return {
...base,
...override,
socialInsurance: { ...base.socialInsurance, ...(override.socialInsurance ?? {}) },
};
}