Add Nuclear RAB levy, metering charge and DUoS catch-all to tariff cost model

The June electricity bill showed £252.72 (7.5% of the bill) in charges the
cost model didn't account for: Nuclear RAB Levy, Metering Charge, and DUoS
Availability/Excess/Reactive. RAB levy and metering charge are modeled
directly (same shape as CCL / standing charge); the DUoS charges need kVA
capacity and kVArh reactive energy that only the supplier's own meter reads,
so they're covered by one adjustable "other network charges" £/month field
to update manually from each bill.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-28 13:07:20 +00:00
parent f1ce0f06d8
commit 3b261a4d8d
10 changed files with 158 additions and 15 deletions

View file

@ -64,7 +64,7 @@ export default function Estimates() {
<>
<div className="info-banner">
Projected cost for the current open period ({report.period.start} to {report.period.end}) trailing average
daily consumption × remaining days, plus standing charge, CCL and VAT for the full period.
daily consumption × remaining days, plus standing charge, CCL, RAB levy, fixed extras and VAT for the full period.
</div>
<div className="stats-strip">
@ -72,6 +72,8 @@ export default function Estimates() {
<div className="stat-box"><div className="stat-value">{formatMoney(report.totals.usage_cost_pence)}</div><div className="stat-label">Usage</div></div>
<div className="stat-box"><div className="stat-value">{formatMoney(report.totals.standing_cost_pence)}</div><div className="stat-label">Standing</div></div>
<div className="stat-box"><div className="stat-value">{formatMoney(report.totals.ccl_cost_pence)}</div><div className="stat-label">CCL</div></div>
<div className="stat-box"><div className="stat-value">{formatMoney(report.totals.rab_levy_cost_pence)}</div><div className="stat-label">RAB Levy</div></div>
<div className="stat-box"><div className="stat-value">{formatMoney(report.totals.metering_cost_pence + report.totals.other_charges_cost_pence)}</div><div className="stat-label">Fixed extras</div></div>
<div className="stat-box"><div className="stat-value">{formatMoney(report.totals.vat_pence)}</div><div className="stat-label">VAT</div></div>
</div>

View file

@ -57,6 +57,8 @@ export default function Reports() {
<div className="stat-box"><div className="stat-value">{formatMoney(report.totals.usage_cost_pence)}</div><div className="stat-label">Usage</div></div>
<div className="stat-box"><div className="stat-value">{formatMoney(report.totals.standing_cost_pence)}</div><div className="stat-label">Standing</div></div>
<div className="stat-box"><div className="stat-value">{formatMoney(report.totals.ccl_cost_pence)}</div><div className="stat-label">CCL</div></div>
<div className="stat-box"><div className="stat-value">{formatMoney(report.totals.rab_levy_cost_pence)}</div><div className="stat-label">RAB Levy</div></div>
<div className="stat-box"><div className="stat-value">{formatMoney(report.totals.metering_cost_pence + report.totals.other_charges_cost_pence)}</div><div className="stat-label">Fixed extras</div></div>
<div className="stat-box"><div className="stat-value">{formatMoney(report.totals.vat_pence)}</div><div className="stat-label">VAT</div></div>
</div>
@ -70,6 +72,7 @@ export default function Reports() {
<tr>
<th>Meter</th><th>Category</th><th className="num">Consumption</th>
<th className="num">Usage</th><th className="num">Standing</th><th className="num">CCL</th>
<th className="num">RAB Levy</th><th className="num">Fixed extras</th>
<th className="num">VAT</th><th className="num">Total</th>
</tr>
</thead>
@ -90,6 +93,8 @@ export default function Reports() {
<td className="num">{formatMoney(m.usage_cost_pence)}</td>
<td className="num">{formatMoney(m.standing_cost_pence)}</td>
<td className="num">{formatMoney(m.ccl_cost_pence)}</td>
<td className="num">{formatMoney(m.rab_levy_cost_pence)}</td>
<td className="num">{formatMoney(m.metering_cost_pence + m.other_charges_cost_pence)}</td>
<td className="num">{formatMoney(m.vat_pence)}</td>
<td className="num">{formatMoney(m.total_pence)}</td>
</tr>
@ -100,6 +105,8 @@ export default function Reports() {
<td className="num">{formatMoney(report.totals.usage_cost_pence)}</td>
<td className="num">{formatMoney(report.totals.standing_cost_pence)}</td>
<td className="num">{formatMoney(report.totals.ccl_cost_pence)}</td>
<td className="num">{formatMoney(report.totals.rab_levy_cost_pence)}</td>
<td className="num">{formatMoney(report.totals.metering_cost_pence + report.totals.other_charges_cost_pence)}</td>
<td className="num">{formatMoney(report.totals.vat_pence)}</td>
<td className="num">{formatMoney(report.totals.total_pence)}</td>
</tr>

View file

@ -16,6 +16,7 @@ const emptyWindow = (label: string, sort: number): WindowForm => ({
const emptyForm = {
id: 0, category_id: 0, name: '', supplier: '', effective_from: new Date().toISOString().slice(0, 10),
effective_to: '', standing_charge_pence_per_day: 0, ccl_rate_pence_per_unit: '' as number | '', ccl_exempt: false,
rab_levy_rate_pence_per_unit: '' as number | '', metering_charge_pence_per_month: 0, other_charges_pence_per_month: 0,
vat_rate_pct: 20, is_time_of_use: false, windows: [emptyWindow('Standard', 0)],
}
@ -55,7 +56,11 @@ export default function Tariffs() {
effective_from: full.effective_from.slice(0, 10), effective_to: full.effective_to ? full.effective_to.slice(0, 10) : '',
standing_charge_pence_per_day: Number(full.standing_charge_pence_per_day),
ccl_rate_pence_per_unit: full.ccl_rate_pence_per_unit != null ? Number(full.ccl_rate_pence_per_unit) : '',
ccl_exempt: full.ccl_exempt, vat_rate_pct: Number(full.vat_rate_pct), is_time_of_use: full.is_time_of_use,
ccl_exempt: full.ccl_exempt,
rab_levy_rate_pence_per_unit: full.rab_levy_rate_pence_per_unit != null ? Number(full.rab_levy_rate_pence_per_unit) : '',
metering_charge_pence_per_month: Number(full.metering_charge_pence_per_month || 0),
other_charges_pence_per_month: Number(full.other_charges_pence_per_month || 0),
vat_rate_pct: Number(full.vat_rate_pct), is_time_of_use: full.is_time_of_use,
windows: windows.length ? windows : [emptyWindow('Standard', 0)],
})
} catch (err) {
@ -109,7 +114,11 @@ export default function Tariffs() {
effective_from: form.effective_from, effective_to: form.effective_to || null,
standing_charge_pence_per_day: form.standing_charge_pence_per_day,
ccl_rate_pence_per_unit: form.ccl_rate_pence_per_unit === '' ? null : form.ccl_rate_pence_per_unit,
ccl_exempt: form.ccl_exempt, vat_rate_pct: form.vat_rate_pct, is_time_of_use: form.is_time_of_use,
ccl_exempt: form.ccl_exempt,
rab_levy_rate_pence_per_unit: form.rab_levy_rate_pence_per_unit === '' ? null : form.rab_levy_rate_pence_per_unit,
metering_charge_pence_per_month: form.metering_charge_pence_per_month,
other_charges_pence_per_month: form.other_charges_pence_per_month,
vat_rate_pct: form.vat_rate_pct, is_time_of_use: form.is_time_of_use,
fallback_split_pct, windows,
}
if (form.id) {
@ -150,7 +159,7 @@ export default function Tariffs() {
<div className="table-wrap">
<table className="data">
<thead>
<tr><th>Name</th><th>Category</th><th>Supplier</th><th>From</th><th>To</th><th>Type</th><th className="num">Standing/day</th><th></th></tr>
<tr><th>Name</th><th>Category</th><th>Supplier</th><th>From</th><th>To</th><th>Type</th><th className="num">Standing/day</th><th className="num">Fixed/month</th><th></th></tr>
</thead>
<tbody>
{tariffs.map(t => (
@ -162,6 +171,7 @@ export default function Tariffs() {
<td>{t.effective_to ? new Date(t.effective_to).toLocaleDateString('en-GB') : <span className="badge badge-outline">open</span>}</td>
<td>{t.is_time_of_use ? <span className="badge badge-tou">TOU</span> : 'Standard'}</td>
<td className="num">{formatMoney(Number(t.standing_charge_pence_per_day))}</td>
<td className="num">{formatMoney(Number(t.metering_charge_pence_per_month) + Number(t.other_charges_pence_per_month))}</td>
<td>{t.window_count} window{t.window_count === 1 ? '' : 's'}</td>
</tr>
))}
@ -227,6 +237,29 @@ export default function Tariffs() {
Climate Change Levy exempt
</label>
<div className="field-row">
<div className="field">
<label>Nuclear RAB levy (pence/unit)</label>
<input type="number" step="0.0001" value={form.rab_levy_rate_pence_per_unit}
onChange={e => setForm({ ...form, rab_levy_rate_pence_per_unit: e.target.value === '' ? '' : parseFloat(e.target.value) })} />
</div>
<div className="field">
<label>Metering charge (£/month)</label>
<input type="number" step="0.01" value={form.metering_charge_pence_per_month / 100}
onChange={e => setForm({ ...form, metering_charge_pence_per_month: (parseFloat(e.target.value) || 0) * 100 })} />
</div>
<div className="field">
<label>Other network charges (£/month)</label>
<input type="number" step="0.01" value={form.other_charges_pence_per_month / 100}
onChange={e => setForm({ ...form, other_charges_pence_per_month: (parseFloat(e.target.value) || 0) * 100 })} />
</div>
</div>
<div className="muted" style={{ marginTop: -8, marginBottom: 14, fontSize: '0.85em' }}>
"Other" is a flat catch-all for DUoS availability/excess/reactive charges these bill against contracted
kVA capacity and kVArh reactive energy from the supplier's own meter, not anything read on-site, so
update it from each bill rather than trying to compute it.
</div>
<label className="field-check" style={{ marginBottom: 14 }}>
<input type="checkbox" checked={form.is_time_of_use} onChange={e => toggleTou(e.target.checked)} />
Time-of-use tariff (split cost across rate windows using a fallback %)

View file

@ -95,6 +95,9 @@ export interface Tariff {
standing_charge_pence_per_day: number
ccl_rate_pence_per_unit: number | null
ccl_exempt: boolean
rab_levy_rate_pence_per_unit: number | null
metering_charge_pence_per_month: number
other_charges_pence_per_month: number
vat_rate_pct: number
is_time_of_use: boolean
fallback_split_pct: Record<string, number>
@ -128,6 +131,9 @@ export interface CostSegment {
usage_cost_pence: number
standing_cost_pence: number
ccl_cost_pence: number
rab_levy_cost_pence: number
metering_cost_pence: number
other_charges_cost_pence: number
vat_pence: number
total_pence: number
}
@ -136,6 +142,9 @@ export interface CostBreakdown {
usage_cost_pence: number
standing_cost_pence: number
ccl_cost_pence: number
rab_levy_cost_pence: number
metering_cost_pence: number
other_charges_cost_pence: number
subtotal_pence: number
vat_pence: number
total_pence: number
@ -164,6 +173,9 @@ export interface ConsumptionCostReport {
usage_cost_pence: number
standing_cost_pence: number
ccl_cost_pence: number
rab_levy_cost_pence: number
metering_cost_pence: number
other_charges_cost_pence: number
vat_pence: number
total_pence: number
}
@ -214,6 +226,9 @@ export interface EstimateReport {
usage_cost_pence: number
standing_cost_pence: number
ccl_cost_pence: number
rab_levy_cost_pence: number
metering_cost_pence: number
other_charges_cost_pence: number
vat_pence: number
}
}