Split cost/estimate across mid-period tariff changes

Rate changes are often scheduled ahead of time or confirmed by finance
after the fact and backdated — assign-tariff already supported any
effective_from date, but cost-calc previously priced a whole period at
a single tariff (whichever was in force on the period-end date),
silently mispricing the days on the other side of a change.

getTariffSegments() finds every tariff assignment overlapping a date
range; getMeterCostForPeriod/getMeterEstimateForPeriod now split
consumption pro-rata by day count across segments and price each
against its own tariff, summing to the period total. Estimates project
the remaining days across a future-scheduled change the same way,
rather than assuming today's tariff holds for the rest of the period.

Also consolidates internal.js's cost/estimate routes (previously their
own duplicate calc) onto the same shared functions reports.js and
estimates.js use, so the Directors' report can't drift from the in-app
numbers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-26 17:43:45 +00:00
parent 4fc5230d79
commit 7ab048931f
5 changed files with 151 additions and 63 deletions

View file

@ -76,7 +76,15 @@ export default function Reports() {
<tbody>
{report.meters.map(m => (
<tr key={m.meter_id}>
<td>{m.meter_name}{!m.has_data && <span className="badge badge-outline" style={{ marginLeft: 6 }}>no data</span>}</td>
<td>
{m.meter_name}
{!m.has_data && <span className="badge badge-outline" style={{ marginLeft: 6 }}>no data</span>}
{m.rate_changed_mid_period && (
<span className="badge badge-tou" style={{ marginLeft: 6 }} title={m.segments.map(s => `${s.tariff_name}: ${s.seg_start} ${s.seg_end}`).join(', ')}>
rate changed
</span>
)}
</td>
<td>{m.category_name}</td>
<td className="num">{formatUnits(m.consumption, m.unit_label)}</td>
<td className="num">{formatMoney(m.usage_cost_pence)}</td>