Berekening: voorbeelden in de keuzegidsen

De uitvoerbare engine-runs achter de huishoud- en productvoorbeelden over capaciteit, dynamische contracten en kopen of wachten.

Dit is de berekening zelf, ongewijzigd overgenomen uit scripts/calibration/guide-examples.ts. Je kunt hem uitvoeren met bun scripts/calibration/guide-examples.ts en de gepubliceerde getallen reproduceren.

/**
 * Reproducible household examples published in the buyer guides.
 *
 * Each example runs the shipped engine against the researched catalogue fleet.
 * The guide assertions in `published-figures.test.ts` format these live results
 * and compare them with the prose, so an engine, catalogue or price change
 * cannot leave an old figure published as if it were still current.
 *
 * Run: bun scripts/calibration/guide-examples.ts
 */

import { computeEngine } from "~/lib/engine"
import type {
  BatteryResult,
  EngineInput,
  EngineResult,
  Recommendation,
  ScenarioResult,
} from "~/lib/engine/types"
import { catalogueFleet } from "~/scripts/calibration/catalogue-fleet"

export const GUIDE_EXAMPLES_REVIEWED_AT = "2026-08-01"

const completeInput = (input: EngineInput): EngineInput => {
  return {
    hasEV: false,
    hasHeatPump: false,
    hasElectricBoiler: false,
    phase: "OnePhase",
    goal: "besparen",
    ...input,
  }
}

const runCatalogue = (input: EngineInput): EngineResult => {
  const track = input.ownership === "huur" ? "PlugIn" : "Vast"

  return computeEngine(completeInput(input), catalogueFleet(track))
}

const batteryBySlug = (result: EngineResult, slug: string): BatteryResult => {
  const battery = result.batteries.find(entry => entry.battery.slug === slug)

  if (!battery) {
    throw new Error(`Guide example battery is no longer ranked: ${slug}`)
  }

  return battery
}

const baseScenario = (battery: BatteryResult): ScenarioResult => {
  const scenario = battery.scenarios.find(entry => entry.scenario === "base")

  if (!scenario) {
    throw new Error(`Guide example has no base scenario: ${battery.battery.slug}`)
  }

  return scenario
}

const requiredNumber = (value: number | null, label: string): number => {
  if (value === null) {
    throw new Error(`Guide example has no numeric ${label}`)
  }

  return value
}

export type PublishedBatteryExample = {
  slug: string
  usableKwh: number
  costCents: number
  paybackBaseYears: number
  paybackFavourableYears: number
  paybackConservativeYears: number
  shiftedYear1Kwh: number
  valueYear1Cents: number
  utilizationYear1KwhPerUsableKwh: number
  effectiveWarrantyYears: number | null
}

const publishedBattery = (battery: BatteryResult): PublishedBatteryExample => {
  const base = baseScenario(battery)

  return {
    slug: battery.battery.slug,
    usableKwh: battery.battery.usableKwh,
    costCents: requiredNumber(base.netCostCents, "cost"),
    paybackBaseYears: requiredNumber(battery.paybackBaseYears, "base payback"),
    paybackFavourableYears: requiredNumber(battery.paybackRangeYears.min, "favourable payback"),
    paybackConservativeYears: requiredNumber(battery.paybackRangeYears.max, "conservative payback"),
    shiftedYear1Kwh: requiredNumber(base.years[0]?.shiftedKwh ?? null, "year-one shift"),
    valueYear1Cents: base.valueYear1Cents,
    utilizationYear1KwhPerUsableKwh: battery.utilizationYear1KwhPerUsableKwh,
    effectiveWarrantyYears: battery.warranty.effectiveWarrantyYears,
  }
}

const capacityInput = completeInput({
  annualConsumptionKwh: 3500,
  pvProductionKwh: 4000,
  roofOrientation: "zuid",
  householdProfile: "gemengd",
  contractType: "vast",
  ownership: "koop",
})

const dynamicContractInput = completeInput({
  annualConsumptionKwh: 3500,
  pvProductionKwh: 3500,
  roofOrientation: "zuid",
  householdProfile: "thuis_overdag",
  ownership: "huur",
})

const buyOrWaitInput = completeInput({
  annualConsumptionKwh: 5000,
  pvProductionKwh: 5000,
  roofOrientation: "zuid",
  householdProfile: "thuis_overdag",
  contractType: "dynamisch",
})

type GuideExamples = {
  capacity: {
    input: EngineInput
    sessy5: PublishedBatteryExample
    sessy10: PublishedBatteryExample
    rankedBest: PublishedBatteryExample
    recommendation: Recommendation | null
  }
  dynamicContract: {
    input: EngineInput
    battery: PublishedBatteryExample
    fixedRecommendation: Recommendation | null
    dynamicRecommendation: Recommendation | null
  }
  buyOrWait: {
    input: EngineInput
    fixed: PublishedBatteryExample
    plugin: PublishedBatteryExample
    fixedRecommendation: Recommendation | null
    pluginRecommendation: Recommendation | null
  }
}

export const runGuideExamples = (): GuideExamples => {
  const capacity = runCatalogue(capacityInput)
  const contractFixed = runCatalogue({ ...dynamicContractInput, contractType: "vast" })
  const contractDynamic = runCatalogue({ ...dynamicContractInput, contractType: "dynamisch" })
  const fixed = runCatalogue({ ...buyOrWaitInput, ownership: "koop" })
  const plugin = runCatalogue({ ...buyOrWaitInput, ownership: "huur" })

  const fixedContractBattery = publishedBattery(
    batteryBySlug(contractFixed, "marstek-venus-e-gen-3"),
  )
  const dynamicContractBattery = publishedBattery(
    batteryBySlug(contractDynamic, "marstek-venus-e-gen-3"),
  )

  if (fixedContractBattery.valueYear1Cents !== dynamicContractBattery.valueYear1Cents) {
    throw new Error("Contract comparison must isolate the decision path from battery economics")
  }

  return {
    capacity: {
      input: capacityInput,
      sessy5: publishedBattery(batteryBySlug(capacity, "sessy-5")),
      sessy10: publishedBattery(batteryBySlug(capacity, "sessy-10")),
      rankedBest: publishedBattery(batteryBySlug(capacity, "solaredge-home-battery-400v")),
      recommendation: capacity.recommendation,
    },
    dynamicContract: {
      input: dynamicContractInput,
      battery: fixedContractBattery,
      fixedRecommendation: contractFixed.recommendation,
      dynamicRecommendation: contractDynamic.recommendation,
    },
    buyOrWait: {
      input: buyOrWaitInput,
      fixed: publishedBattery(batteryBySlug(fixed, "solaredge-home-battery-400v")),
      plugin: publishedBattery(batteryBySlug(plugin, "marstek-venus-e-gen-3")),
      fixedRecommendation: fixed.recommendation,
      pluginRecommendation: plugin.recommendation,
    },
  }
}

if (import.meta.main) {
  console.log(JSON.stringify(runGuideExamples(), null, 2))
}