diff --git a/src/modules/estimateWizard/components/TenYearValueChart.jsx b/src/modules/estimateWizard/components/TenYearValueChart.jsx
index ab6b088..7e948a8 100644
--- a/src/modules/estimateWizard/components/TenYearValueChart.jsx
+++ b/src/modules/estimateWizard/components/TenYearValueChart.jsx
@@ -1,52 +1,128 @@
/**
- * TenYearValueChart — upfront vs illustrative 10-yr savings, per option (spec §10).
- * Savings bars only render when a premium was provided and conservative mode is off.
+ * TenYearValueChart — 10-year value comparison panel (spec §10).
+ *
+ * Honors the full §10 brief:
+ * - shows ASSUMPTIONS per tier (discount %, service life, hail positioning, value view)
+ * - lets the customer TOGGLE discount scenarios (low / base / high)
+ * - separates upfront cost from lifecycle value (breakeven + lifecycle delta)
+ * - never claims a guaranteed discount; all savings labeled illustrative
+ *
+ * Savings/breakeven/lifecycle only render when a premium was provided and
+ * conservative mode is off; otherwise the panel falls back to assumptions only.
*/
-import React from 'react';
+import React, { useState } from 'react';
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend, Cell } from 'recharts';
const PKG_LABEL = { GOOD: 'Good', BETTER: 'Better', BEST: 'Best' };
const BAR_COLOR = { GOOD: '#a1a1aa', BETTER: '#3b82f6', BEST: '#fda913' };
+const SCENARIOS = [
+ { key: 'low', label: 'Low' },
+ { key: 'base', label: 'Base' },
+ { key: 'high', label: 'High' },
+];
+
+const usd = (n) => `$${Number(n).toLocaleString('en-US')}`;
const TenYearValueChart = ({ valueModel }) => {
+ const [scenario, setScenario] = useState('base');
+ const showSavings = valueModel.premiumKnown && !valueModel.conservativeMode;
+
const data = valueModel.rows.map((r) => ({
name: PKG_LABEL[r.type],
type: r.type,
Upfront: r.price.base,
- 'Est. 10-yr savings': r.savings ? r.savings.base : 0,
+ 'Est. 10-yr savings': r.savings ? r.savings[scenario] : 0,
}));
- const fmt = (n) => `$${(n / 1000).toFixed(0)}k`;
+ const fmtK = (n) => `$${(n / 1000).toFixed(0)}k`;
return (
-
10-year value comparison
-
+
+
10-year value comparison
+ {showSavings && (
+
+ Discount scenario
+ {SCENARIOS.map((s) => (
+ setScenario(s.key)}
+ className={`px-2.5 py-1 rounded-lg text-[11px] font-semibold transition-all
+ ${scenario === s.key
+ ? 'bg-amber-500 text-white shadow'
+ : 'bg-zinc-100 dark:bg-white/5 text-zinc-500 dark:text-zinc-400 hover:text-amber-600'}`}
+ >
+ {s.label}
+
+ ))}
+
+ )}
+
+
+ {/* Upfront vs illustrative savings */}
+
-
+
`$${Number(v).toLocaleString('en-US')}`}
+ formatter={(v) => usd(v)}
contentStyle={{ borderRadius: 12, border: '1px solid #e4e4e7', fontSize: 12 }}
/>
{data.map((d) => | )}
- {valueModel.premiumKnown && !valueModel.conservativeMode && (
+ {showSavings && (
)}
-
- {valueModel.premiumKnown
- ? 'Savings shown are illustrative and depend on carrier approval, policy terms, and documentation.'
- : 'Add your insurance premium earlier in the wizard to see an illustrative 10-year savings scenario.'}
+
+ {/* Assumptions + lifecycle metrics per tier */}
+
+ {valueModel.rows.map((r) => (
+
+
{PKG_LABEL[r.type]}
+
+
+
+
+
+ {showSavings && (
+ <>
+
+
+ {/* Breakeven only when pricing AND discount known (spec §10 display rule). */}
+
+
+ >
+ )}
+
+ ))}
+
+
+
+ {showSavings
+ ? 'Savings scenarios are illustrative model estimates — not guaranteed. They depend on carrier approval, policy terms, state rules, and documentation. Lifecycle value delta is a model estimate, not a promise.'
+ : valueModel.conservativeMode
+ ? 'Savings scenarios are hidden. Figures show upfront cost and durability assumptions only.'
+ : 'Add your insurance premium earlier in the wizard to see illustrative 10-year savings, breakeven, and lifecycle value scenarios.'}
);
};
+const Metric = ({ label, value, accent, muted, hint }) => (
+
+ {label}
+
+ {value}
+ {hint && {hint} }
+
+
+);
+
export default TenYearValueChart;
diff --git a/src/modules/estimateWizard/engine/valueModel.js b/src/modules/estimateWizard/engine/valueModel.js
index a87f017..11bfd76 100644
--- a/src/modules/estimateWizard/engine/valueModel.js
+++ b/src/modules/estimateWizard/engine/valueModel.js
@@ -3,63 +3,82 @@
*
* Makes premium options understandable WITHOUT pretending savings are guaranteed:
* - 10-yr premium savings scenario = annual premium × assumed discount × 10
- * - shows low/base/high range (carrier discount is never certain)
+ * - computes low/base/high scenarios so the UI can TOGGLE between them (carrier
+ * discount is never certain)
* - upgrade breakeven only when pricing AND discount are known
* - lifecycle value delta is labeled a model estimate, not a promise
+ * - per-tier "10-year value view" positioning (Upfront only / Savings + protection /
+ * Savings + longer lifecycle + protection)
*
* Pure function. Honors conservativeMode (spec §18) by suppressing savings claims.
*/
import { pricePackage } from './pricing';
const YEARS = 10;
+const SCEN_KEYS = ['low', 'base', 'high'];
+
+// 10-year value view positioning per tier (spec §10 comparison table).
+const VALUE_VIEW = {
+ GOOD: 'Upfront only',
+ BETTER: 'Savings + protection',
+ BEST: 'Savings + longer lifecycle + protection',
+};
export function computeTenYearValue({ inputs, tenantConfig }) {
const { packages, pricing, discountScenarios, conservativeMode } = tenantConfig;
const premium = (!inputs.premiumUnknown && inputs.premiumAmount > 0) ? inputs.premiumAmount : null;
+ const showSavings = !!premium && !conservativeMode;
- const goodPrice = pricePackage(packages.find((p) => p.type === 'GOOD'), pricing).base;
+ const goodPkg = packages.find((p) => p.type === 'GOOD');
+ const goodPrice = pricePackage(goodPkg, pricing).base;
+ const goodLife = goodPkg?.serviceLifeYears || 28;
const rows = packages.map((pkg) => {
const price = pricePackage(pkg, pricing);
const scen = discountScenarios[pkg.type] || { low: 0, base: 0, high: 0 };
- // 10-year premium savings scenario (illustrative).
- const savings = (premium && !conservativeMode)
- ? {
- low: Math.round(premium * (scen.low / 100) * YEARS),
- base: Math.round(premium * (scen.base / 100) * YEARS),
- high: Math.round(premium * (scen.high / 100) * YEARS),
- }
- : null;
-
// Upgrade cost vs the Good baseline.
const upgradeCost = Math.max(0, price.base - goodPrice);
- // Breakeven (years) — only when both pricing and a positive savings scenario exist.
- const annualSavingsBase = (premium && !conservativeMode) ? premium * (scen.base / 100) : 0;
- const breakevenYears = (upgradeCost > 0 && annualSavingsBase > 0)
- ? +(upgradeCost / annualSavingsBase).toFixed(1)
- : null;
-
- // Lifecycle value delta (model estimate): savings + avoided future roof reserve − upgrade cost.
- // Avoided-reserve proxy: extra service life beyond Good, valued at Good's price prorated.
- const goodLife = packages.find((p) => p.type === 'GOOD').serviceLifeYears || 28;
+ // Avoided future roof-cycle reserve proxy: extra service life beyond Good,
+ // valued at Good's price prorated (labeled a model estimate downstream).
const extraLife = Math.max(0, (pkg.serviceLifeYears || goodLife) - goodLife);
const avoidedReserve = Math.round((extraLife / goodLife) * goodPrice * 0.5);
- const lifecycleDelta = (savings ? savings.base : 0) + avoidedReserve - upgradeCost;
+
+ // Per-scenario savings / annual / breakeven / lifecycle delta so the UI can
+ // toggle low/base/high (spec §10: "allow toggling discount scenarios").
+ const savings = {}, annualSavings = {}, breakeven = {}, lifecycleDelta = {};
+ SCEN_KEYS.forEach((k) => {
+ const pct = scen[k] || 0;
+ const annual = showSavings ? premium * (pct / 100) : 0;
+ const tenYr = Math.round(annual * YEARS);
+ annualSavings[k] = Math.round(annual);
+ savings[k] = tenYr;
+ // Breakeven (years) — only when both pricing and a positive discount exist.
+ breakeven[k] = (upgradeCost > 0 && annual > 0) ? +(upgradeCost / annual).toFixed(1) : null;
+ // Lifecycle value delta = savings + avoided reserve − upgrade cost.
+ lifecycleDelta[k] = Math.round(tenYr + avoidedReserve - upgradeCost);
+ });
return {
type: pkg.type,
name: pkg.name,
price,
+ // Assumptions surfaced to the customer (spec §10 "show assumptions").
+ discountScenario: scen, // { low, base, high } discount % assumption
discountPct: scen.base,
serviceLifeYears: pkg.serviceLifeYears,
hailResistance: pkg.hailResistance,
- savings,
+ valueView: VALUE_VIEW[pkg.type] || '',
upgradeCost,
- breakevenYears,
avoidedReserve,
- lifecycleDelta: Math.round(lifecycleDelta),
+ // Per-scenario outputs (null savings when not shown, for back-compat callers).
+ savings: showSavings ? savings : null,
+ annualSavings,
+ breakeven,
+ lifecycleDelta,
+ // Back-compat scalar (base scenario).
+ breakevenYears: breakeven.base,
};
});
@@ -67,6 +86,7 @@ export function computeTenYearValue({ inputs, tenantConfig }) {
years: YEARS,
premiumKnown: !!premium,
conservativeMode: !!conservativeMode,
+ scenarios: SCEN_KEYS,
rows,
};
}
diff --git a/src/modules/estimateWizard/utils/wizardReportExport.js b/src/modules/estimateWizard/utils/wizardReportExport.js
index 3b3622d..4b23c77 100644
--- a/src/modules/estimateWizard/utils/wizardReportExport.js
+++ b/src/modules/estimateWizard/utils/wizardReportExport.js
@@ -148,21 +148,45 @@ export async function generateReportPdf(session, tenantConfig) {
// ── 10-year comparison ──
heading('10-year value comparison');
+
+ // Assumptions table (spec §10 "show assumptions").
autoTable(doc, {
startY: y, margin: { left: MARGIN, right: MARGIN },
- head: [['Option', 'Upfront (base)', 'Est. 10-yr savings', 'Service life']],
+ head: [['Option', 'Upfront (base)', 'Discount assumption', 'Service life', 'Hail resistance', '10-yr value view']],
body: m.value.rows.map((r) => [
PKG_LABEL[r.type],
formatUSD(r.price.base),
- r.savings ? `${formatUSD(r.savings.low)}–${formatUSD(r.savings.high)}` : '—',
+ `${r.discountScenario?.base ?? 0}% (if eligible)`,
`${r.serviceLifeYears}+ yrs`,
+ r.hailResistance,
+ r.valueView,
]),
- styles: { fontSize: 8.5, cellPadding: 2 },
+ styles: { fontSize: 8, cellPadding: 1.8 },
+ headStyles: { fillColor: [24, 24, 27], textColor: 255 },
+ });
+ y = doc.lastAutoTable.finalY + 4;
+
+ // Value outputs table — savings range, breakeven, lifecycle delta (model estimate).
+ autoTable(doc, {
+ startY: y, margin: { left: MARGIN, right: MARGIN },
+ head: [['Option', 'Est. 10-yr savings (low–high)', 'Upgrade breakeven', 'Lifecycle value delta (model est.)']],
+ body: m.value.rows.map((r) => [
+ PKG_LABEL[r.type],
+ r.savings ? `${formatUSD(r.savings.low)}–${formatUSD(r.savings.high)}` : '—',
+ r.breakeven?.base != null ? `${r.breakeven.base} yrs` : '—',
+ r.savings ? formatUSD(r.lifecycleDelta?.base ?? 0) : '—',
+ ]),
+ styles: { fontSize: 8, cellPadding: 1.8 },
headStyles: { fillColor: [24, 24, 27], textColor: 255 },
});
y = doc.lastAutoTable.finalY + 6;
doc.setFont('helvetica', 'italic'); doc.setFontSize(7.5); doc.setTextColor(120);
- text('Savings shown are illustrative and must be confirmed with your insurance carrier.', MARGIN, y); y += 8;
+ doc.splitTextToSize(
+ 'Savings scenarios are illustrative model estimates, not guaranteed. They depend on carrier approval, policy '
+ + 'terms, state rules, and documentation. Breakeven and lifecycle value delta are model estimates, not promises.',
+ W - MARGIN * 2,
+ ).forEach((ln) => { ensure(5); text(ln, MARGIN, y); y += 4.2; });
+ y += 4;
// ── Selected add-ons ──
heading('Selected add-ons');