Commit graph

8 commits

Author SHA1 Message Date
bed0b134dc Fix estimate silently dropping the gap between last reading and today
remainingDays was computed as days-from-today to period-end, while
actual-to-date consumption only ever reflects data up to the meter's
last real reading. With sparse readings the last reading sits days
before today, so that gap was counted in neither the actual nor the
projected total. Project from the last reading's date instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 14:04:49 +00:00
0b9ac16449 Interpolate period boundaries for historical consumption reports
Reports previously used the raw delta between whichever real readings
happened to fall nearest a period's start/end, un-prorated — accurate
only when readings land close to calendar boundaries. With sparse
readings this misattributes days on either side to the neighbouring
period instead.

getPeriodConsumption() now interpolates the meter's value at each
boundary between the readings bracketing it (linear by day-count),
then diffs those — equivalent to a "daily rate carried forward until
the next reading" spreadsheet approach, generalised across however
many sparse-reading gaps a period spans. Falls back to the old
nearest-reading behaviour when a boundary can't be interpolated (period
starts before the first-ever reading or ends after the latest one).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 13:51:10 +00:00
5711e11a07 Commit missing MQTT/history/scheduler files referenced by meters.js
routes/meters.js already imported lib/mqtt.js and lib/history.js in an
earlier uncommitted change; a prior commit staged the whole meters.js
file (picking up those imports) without staging the modules themselves,
crashing production with ERR_MODULE_NOT_FOUND. Completing the commit:
package.json (mqtt dependency), index.js (wires connectMqtt/startScheduler,
both already error-guarded if the broker isn't reachable), and the three
lib files themselves.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 13:32:22 +00:00
67dd943cdb Convert gas meter volume readings to kWh for cost calculations
Gas meters are read in raw volume (ft3 on our imperial meter, m3 on
newer metric ones), never kWh — but tariff rates and CCL/RAB levy are
all p/kWh. Add gas_volume_unit + gas_calorific_value to meters, and
convert reading deltas to kWh in cost-calc.js using the standard
UK gas-billing formula (matches the exact calculation printed on
supplier invoices) before any cost math runs.

Also fixes meter/readings pages showing raw gas dial readings
mislabeled as "kWh" — they now show the actual physical unit
(ft³/m³) via a new reading_unit_label, while computed consumption
stays correctly labeled kWh.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 13:25:03 +00:00
3b261a4d8d 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>
2026-07-28 13:07:20 +00:00
f1ce0f06d8 Fix TOU rate-window mismatch and enforce fallback-split validation
Rate windows were matched by substring (label.includes(key)), so
overlapping labels like "Day"/"Weekday" could silently cross-match to
the wrong rate. Switched to an exact match.

The "split must total 100%" check was advisory-only in the UI and
never validated server-side, letting a tariff save with a split that
doesn't sum to 100% and permanently mis-cost that meter's usage.
Enforced on both the API (POST/PATCH /api/tariffs) and the Save
button.

Also aligned the app's portal category to 'Hotel' (already correct in
auth/src/db.js) here and in stack-init/install-stack.sh, which had
both drifted to 'Operations'.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-28 12:59:19 +00:00
7ab048931f 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>
2026-07-26 17:43:45 +00:00
4fc5230d79 Initial commit — utilities app (meter readings, tariffs, cost tracking)
Fastify + pg backend, React/TS/Vite frontend. Categories (electric,
gas, oil, water), meters with sub-metering rollup, tariffs with
time-of-use rate windows, standing charges, Climate Change Levy and
VAT, manual reading entry, consumption/cost reports, period cost
estimates, and an API-key-gated /api/internal/* surface for the
reports app's Directors Report.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 17:33:42 +00:00