diff --git a/frontend/src/__tests__/dashboard.test.ts b/frontend/src/__tests__/dashboard.test.ts index 6167608a8..679c6185a 100644 --- a/frontend/src/__tests__/dashboard.test.ts +++ b/frontend/src/__tests__/dashboard.test.ts @@ -153,6 +153,23 @@ describe('Dashboard Module', () => { }); describe('loadDashboard', () => { + // #1709: committed_monthly sums the active commitments' estimated monthly + // savings (handler_dashboard.go), not what they cost, so the card must not + // call it "committed". + test('Active Commitments detail names the figure as estimated savings', async () => { + (api.getDashboardSummary as jest.Mock).mockResolvedValue({ + total_recommendations: 0, active_commitments: 3, committed_monthly: 500, + current_coverage: 70, target_coverage: 80, ytd_savings: 0, by_service: {}, + }); + (api.getUpcomingPurchases as jest.Mock).mockResolvedValue({ purchases: [] }); + + await loadDashboard(); + + const tile = document.querySelector('#summary [data-kpi="commitments"]'); + expect(tile?.textContent).toContain('$500/mo estimated savings'); + expect(tile?.textContent).not.toContain('committed'); + }); + test('fetches dashboard summary and upcoming purchases', async () => { (api.getDashboardSummary as jest.Mock).mockResolvedValue({ potential_monthly_savings: 1000, diff --git a/frontend/src/dashboard.ts b/frontend/src/dashboard.ts index 12fe45c1d..381ae3cf1 100644 --- a/frontend/src/dashboard.ts +++ b/frontend/src/dashboard.ts @@ -404,8 +404,10 @@ function renderDashboardSummary( // #180: count the same rows the range sums (and the Opportunities page // counts); the backend total ignores the Enabled Providers setting. detail: `${recsCount ?? data.total_recommendations ?? NO_VALUE} recommendations` }, + // #1709: committed_monthly is the active commitments' estimated monthly + // savings, not what they cost. { kpi: 'commitments', title: 'Active Commitments', value: data.active_commitments != null ? String(data.active_commitments) : NO_VALUE, - detail: `${formatCurrency(data.committed_monthly)}/mo committed` }, + detail: `${formatCurrency(data.committed_monthly)}/mo estimated savings` }, { kpi: 'coverage', title: 'Current Coverage', value: coverageValue, detail: coverageDetail }, { kpi: 'ytd', title: 'YTD Savings', value: formatCurrency(data.ytd_savings), valueSavings: true, detail: 'From commitment purchases' }, diff --git a/internal/api/openapi.yaml b/internal/api/openapi.yaml index cb971c99e..ec6490641 100644 --- a/internal/api/openapi.yaml +++ b/internal/api/openapi.yaml @@ -6771,6 +6771,9 @@ components: committed_monthly: type: number format: double + description: >- + Estimated monthly savings of the active commitments (the sum of + their estimated savings), not what the commitments cost. current_coverage: type: number format: double diff --git a/internal/api/types.go b/internal/api/types.go index c64297cc9..bb6066b74 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -664,7 +664,9 @@ type DashboardSummaryResponse struct { PotentialMonthlySavings float64 `json:"potential_monthly_savings"` TotalRecommendations int `json:"total_recommendations"` ActiveCommitments int `json:"active_commitments"` - CommittedMonthly float64 `json:"committed_monthly"` + // CommittedMonthly is the estimated monthly savings of the active + // commitments (the sum of their EstimatedSavings), not their cost (#1709). + CommittedMonthly float64 `json:"committed_monthly"` // CurrentCoverage is null when there is neither committed nor potential // savings to measure (0/0), instead of a fabricated 100% (issue #72). CurrentCoverage *float64 `json:"current_coverage"`