Pull utilities readings/costs/estimate into Directors Forecast report

Adds a utilities-client.js mirroring the existing forecasting-client
HTTP pattern (UTILITIES_URL/UTILITIES_API_KEY). Directors report
response gains a separate `utilities` field; degrades gracefully with
an error string if the utilities app is unreachable or unconfigured.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-26 17:33:35 +00:00
parent 21224c0ebe
commit 9e9e1d7400
2 changed files with 54 additions and 0 deletions

View file

@ -0,0 +1,33 @@
// Client for the `utilities` app's internal API (meter readings, tariffs, energy costs).
// Mirrors the existing forecasting integration pattern used in
// routes/directors-forecast.js and routes/weekly-actual.js (X-API-Key header,
// base URL + key from env, throw on non-2xx).
async function utilFetch(path) {
const apiKey = process.env.UTILITIES_API_KEY
const baseUrl = process.env.UTILITIES_URL || 'http://10.10.10.127:3080'
if (!apiKey) throw new Error('UTILITIES_API_KEY not configured')
const res = await fetch(`${baseUrl}${path}`, {
headers: { 'X-API-Key': apiKey },
})
if (!res.ok) throw new Error(`Utilities API ${res.status}${path}`)
return res.json()
}
// GET /api/internal/readings?period=YYYY-MM&category=<category> — raw reading summary
export async function getUtilityReadings(period, category) {
const qs = new URLSearchParams({ period })
if (category) qs.set('category', category)
return utilFetch(`/api/internal/readings?${qs.toString()}`)
}
// GET /api/internal/costs?period=YYYY-MM — cost breakdown by category (usage/standing/CCL/VAT/total)
export async function getUtilityCosts(period) {
const qs = new URLSearchParams({ period })
return utilFetch(`/api/internal/costs?${qs.toString()}`)
}
// GET /api/internal/estimate?period=current — projected cost for the open period
export async function getUtilityEstimate() {
return utilFetch(`/api/internal/estimate?period=current`)
}

View file

@ -1,5 +1,6 @@
import { requireAuth, hasCap } from '../auth.js'
import { pool, getSetting } from '../db.js'
import { getUtilityReadings, getUtilityCosts, getUtilityEstimate } from '../lib/utilities-client.js'
async function fcFetch(path) {
const apiKey = await getSetting('forecasting_api_key') || process.env.FORECASTING_API_KEY
@ -274,6 +275,25 @@ export async function directorsForecastRoutes(fastify) {
[session.id]
)
// ── Utilities (meter readings/costs/estimate from the `utilities` app) ────
// Fetched best-effort — a failure here (e.g. utilities app not deployed
// yet, or key not configured) must not break the rest of the report.
let utilities = { readings: null, costs: null, estimate: null, error: null }
try {
const period = `${year}-${String(month).padStart(2, '0')}`
const now = new Date()
const isCurrentPeriod = year === now.getFullYear() && month === now.getMonth() + 1
const [readings, costs, estimate] = await Promise.all([
getUtilityReadings(period),
getUtilityCosts(period),
isCurrentPeriod ? getUtilityEstimate() : Promise.resolve(null),
])
utilities = { readings, costs, estimate, error: null }
} catch (err) {
utilities = { readings: null, costs: null, estimate: null, error: err.message }
}
return {
year, month,
forecast: {
@ -295,6 +315,7 @@ export async function directorsForecastRoutes(fastify) {
pickup: { rooms: totalPickup, accomm: totalPickup * sessionRate, avg_rate: sessionRate },
weekly: Object.entries(weekBands).map(([label, v]) => ({ label, ...v })),
snapshots: snapshotsRes.rows,
utilities,
}
})