// fpc 端到端测试:计价引擎单元断言 + (可选)真实图纸识别 import { buildQuote, kitchenBillable, tierUnitPrice, applyOps, withinAuthority } from '../src/quote.js'; import { normalizeCabinetName } from '../src/vision.js'; import { renderSVG, buildPlan } from '../src/redraw.js'; import { renderReport } from '../src/report.js'; import assert from 'node:assert'; let pass = 0; let fail = 0; function t(name, fn) { try { fn(); pass++; console.log(' ✓', name); } catch (e) { fail++; console.error(' ✗', name, '→', e.message); } } console.log('== 计价引擎 =='); t('厨房U型计费米数: 长3.5宽2.4 → 扣2转角+冰箱 = 7.2m', () => { const kb = kitchenBillable([3.5, 2.4, 3.5, 2.4]); assert.equal(kb.kitchenType, 'U'); assert.equal(kb.billable, 7.2); }); t('厨房L型计费米数: 长3.6宽1.8 → 扣1转角+冰箱 = 3.8m', () => { const kb = kitchenBillable([3.6, 1.8, 0, 0]); assert.equal(kb.kitchenType, 'L'); assert.equal(kb.billable, 3.8); }); t('PET档15%浮动单价 = 区间上限5000', () => { assert.equal(tierUnitPrice({ name: 'PET', min: 4500, max: 5000 }, 15), 5000); }); t('PET档10%浮动单价 = 区间下限4500', () => { assert.equal(tierUnitPrice({ name: 'PET', min: 4500, max: 5000 }, 10), 4500); }); t('快速报价: 3.6×2.6衣柜@5000 = 46800', () => { const q = buildQuote({ cabinets: [{ type: '主卧衣柜', width: 3.6, height: 2.6 }], tierName: 'PET', floatPct: 15 }); assert.equal(q.rooms[0].total, 46800); }); t('价格型客户默认85折+自动活动', () => { const q = buildQuote({ cabinets: [{ type: '主卧衣柜', width: 3.6, height: 2.6 }], tierKey: 'price', floatPct: 15 }); assert.equal(q.meta.discount, 0.85); assert.ok(q.activityLines.length >= 1, '活动应自动启用'); }); t('品质型客户 +15% 品质系数', () => { const q = buildQuote({ cabinets: [{ type: '主卧衣柜', width: 3.6, height: 2.6 }], tierKey: 'quality', floatPct: 15 }); assert.equal(q.meta.premiumRate, 1.15); }); t('增删项 ops: add+remove 生效', () => { const out = applyOps([{ type: '主卧衣柜', width: 3, height: 2.6 }, { type: '书房书柜', width: 2, height: 2.6 }], [{ op: 'remove', target: '书房书柜' }, { op: 'add', type: '阳台柜', width: 1.8 }]); assert.deepEqual(out.map((c) => c.type), ['主卧衣柜', '阳台柜']); }); t('谈价区间 = 到手价 ± 浮动率', () => { const q = buildQuote({ cabinets: [{ type: '玄关鞋柜', width: 1.2, height: 2.4 }], floatPct: 15 }); const f = q.totals.finalPrice; assert.equal(q.totals.range.low, Math.round(f * 0.85)); assert.ok(withinAuthority(q, f)); assert.ok(!withinAuthority(q, f * 1.2), '超出区间的价格应在授权外'); }); console.log('== 名称归一 =='); t('厨房吊柜→厨房橱柜(强制延米计价)', () => { assert.equal(normalizeCabinetName('厨房吊柜'), '厨房橱柜'); assert.equal(normalizeCabinetName('开放式厨房柜'), '厨房橱柜'); }); t('玄关柜/入户柜→玄关鞋柜', () => { assert.equal(normalizeCabinetName('玄关柜'), '玄关鞋柜'); assert.equal(normalizeCabinetName('入户柜'), '玄关鞋柜'); }); console.log('== 报告渲染 =='); t('renderReport 产出含引擎与交互的HTML', () => { const q = buildQuote({ cabinets: [{ type: '主卧衣柜', width: 3.2, height: 2.6 }], tierKey: 'standard' }); const plan = buildPlan({ analysis: { rooms: [{ name: '主卧', pos: '左中', approxArea: 14 }] }, cabinets: q.rooms.map((r) => ({ type: r.type, width: 3.2, height: 2.6, pos: '左中' })) }); const svg = renderSVG(plan); const html = renderReport({ quote: q, planSVG: svg, clientName: '测试', salesNote: 'x' }); assert.ok(html.includes('id="finalPrice"')); assert.ok(html.includes('function buildQuote'), '计价引擎应内嵌'); assert.ok(svg.startsWith('