Skip to content

Repository files navigation

FFCalc

Picking this repo up, or reusing it for another version of the app? Start with HANDOFF.md. It lists the open questions that need a human answer, the behaviour that looks like a bug but is correct, and the hazards in reusing this code. Agents: CLAUDE.md has the short form.

Building the web version? Read HANDOFF-web.md first. src/ is a framework-agnostic engine with zero runtime dependencies and is meant to be reused as-is; ui/ models a funding schedule that no longer exists and should not be ported.

Status: retiring. v0.9.0 is the final desktop release. The engine is being carried forward into a web application — see HANDOFF-web.md — after which this repository will be deleted. Take a copy before that happens: the rate tables can be rebuilt from Monitor's SQL, but the reconciliation suite and the reasoning in the handoffs cannot.

Interactive calculator a NZ PHO uses with practices to estimate practice income under a bulk-funding arrangement. Built for thePHO.

The engine ingests a practice's enrolled demographics and returns Health NZ → PHO revenue per stream; the UI then layers on per-stream pass-through (retention) sliders so finance staff can model the offer to the practice.

The 1 July 2026 schedule change

Health NZ replaced first-level capitation with a needs-adjusted schedule on 1 July 2026. This is the single most important fact about the repo.

to 30 Jun 2026 from 1 Jul 2026
Age bands 6 13
Rate dimensions age × sex × HUHC × formula age × sex × deprivation quintile × P3 group × GCH group
Funding formulas Interim and LCAF Interim only — no Access / Non-Access rate split
HUHC on first-level higher rate no effect
Under-6s scheme paid $0 — retired
Source table size 768 rows 18,720 rows

HOP, SIA and the CarePlus percentages were not restructured and still key on the six legacy age bands, which is why those three reconcile to the cent while the others needed rebuilding.

Modelling the old schedule understated a practice by 3–9%, varying with its high-needs mix — so no correction factor fixes it. This is what RELEASE_NOTES.md v0.9.0 addresses.

Funding streams

Stream Rate file Drives
First-Level Services (GMS) capitation-fy27.json Every enrolled patient, by the five-dimension key above
CSC top-up capitation-fy27.json CSC holders at practices on a CSC funding model; $0 under age 5
Zero Fees Under-14s capitation-fy27.json Enrolled patients aged 0–13
Zero Fees Under-6s capitation-fy27.json Retired — pays $0
VLCA capitation-fy27.json VLCA practices; the rate differs for CSC holders
Contingent (Enhanced Capitation) capitation-fy27.json Practices on an EC funding model
Health Promotion (HOP) hop.json Ethnicity × deprivation band; HUHC excluded
Services to Improve Access (SIA) sia.json Age × sex × ethnicity × deprivation band; HUHC excluded
CarePlus careplus.json Expected CarePlus patients
Management Services management.json The PHO's total enrolment. Not a practice stream

The first six share one primary key, so they share one rate file rather than duplicating that key five times with nothing keeping the copies in step.

Which supplements a practice gets

FundingModel replaces the old practiceType: Access | Non-Access toggle, which modelled a rate split that no longer exists. It mirrors Monitor's PracticeFundingParameters.FundingModel string — CSC14E means CSC top-up + Under-14s + Enhanced Capitation. GMS, HOP, SIA and CarePlus are never gated by it.

CarePlus is not a per-enrollee capitation

Every other stream pays per enrolled person. CarePlus pays on expected CarePlus patients:

payment = ROUND(Σ every patient's CarePlus percentage) × rate

Two consequences that look like bugs and are not:

  • HUHC percentages are negative (−0.9950 to −0.5900) and are part of the sum. That is how the payer excludes HUHC holders. Applying the non-HUHC percentages to everyone overstates the stream by ~3%.
  • The total is rounded to whole patients, because the payer rounds. StreamResult.units.count keeps the unrounded expectation so the basis stays visible.

The workbook's own "Care Plus Potential" figure is not the basis — it applies the non-HUHC percentages to the whole roster. The Residual is what is paid.

Getting a practice's population in

Normally you just pick the practice from a list. A prebuilt dataset carries every thePHO practice, and the app selects from it:

const ds = loadPracticeDataset(JSON.parse(file));   // or demoPracticeDataset()
listPractices(ds);                                  // -> [{ id, name, pho, patients }]
const { mix, warnings } = practiceMix(ds, 'kowhai-street-surgery');
calculateAll(mix);

A dataset stores each practice's published margins — about 350 numbers — not an expanded population, so a whole PHO is well under a megabyte. snapshotToMix re-expands them using the very same code the workbook importer uses, so a dataset built last month and a workbook read today cannot reconstruct differently. There is a test asserting the two agree to the cent.

Build one with npm run dataset -- <workbook-dir>, pointed at a directory of delivered workbooks laid out one folder per PHO. The output is not publishable — it describes real practices and their funding, this repository is public, and both practice-dataset.json and private/ are gitignored. Distribute it the way the underlying reports are already distributed and let those access controls govern it.

With no real dataset loaded the app falls back to demoPracticeDataset() — six invented practices in an invented PHO, generated from smooth models rather than perturbed from any real roster. Its reported figures are deliberately zero so nothing looks like a real number to reconcile against.

Reading a workbook directly

Still supported, and it is what the dataset generator uses. Reconstructing a 1,170-cell rate surface from whole-practice totals under an independence assumption is not accurate enough — it misses SIA by +5% and CarePlus by +6% on its own — so importRegisterAnalysis reads the practice's real cross-tab out of its monthly Register Analysis workbook:

  • Funding Details publishes, per age band × sex, that cell's patients by deprivation quintile, by P3 group and by GCH group — three margins of one 5 × 3 × 3 table, so the joint is recovered by iterative proportional fitting rather than assumed.
  • Patient Demographics gives ethnicity by age band.
  • Patient Enrolments gives the CSC and HUHC totals. These are the only inputs with no breakdown at all; CSC's shape comes from a national holding profile (src/reference/csc-profile.json) scaled so the practice's own total is reproduced exactly.

mixFromMarginals still exists for a hypothetical practice with no workbook, and its docstring says plainly what it costs you.

The workbook publishes two different "Quintile 5" counts and they disagree. Patient Demographics shows the NZDep2023 demographic quintile; Funding Details shows the funding quintile, and on a real report they differ by well over 10%. Only the funding one is paid on. Reading the demographics figure overstates SIA by about 5%.

Reconciliation

tests/reconcile.deliveredReports.test.ts runs against delivered Register Analysis workbooks: every practice in a PHO imported, computed, and checked against that PHO's roll-up. Both sides are read from the workbooks at run time, so there is no transcribed copy of the truth to drift — and no practice's figures end up in a tracked file.

The workbooks themselves are not in this repository, which is public; put them in the gitignored private/register-analysis/ to run the suite. Without them it skips and the rest of the tests still pass.

stream PHO-level variance
first-level −0.04%
HOP −0.05%
SIA −0.25%
U14 −0.04%
CSC top-up +0.53%
contingent −0.04%
CarePlus +0.10%
total +0.06%

Given the true patient-level cross-tab instead of the workbook reconstruction, the same engine reproduces that PHO's monthly payment to well under one dollar in a million — so essentially all of the residual above is the reconstruction, not the rates.

Distribution

Tauri-packaged desktop app (Windows .exe, Linux AppImage). Local-only: practice data stays in IndexedDB, no network calls at runtime.

There is now exactly one engine. ui/engine-entry.ts bundles src/ straight into the page with the esbuild the build already used, so the hand-maintained ui/engine.js is gone — along with the drift that let the UI ship a different answer from the tested engine.

The app opens on a practice picker rather than a data-entry form: pick a practice, adjust its mix with multipliers, and read the nine streams. Everything a user can change is applied to the practice's published margins and expanded through the same code that reconciles against the delivered reports.

For deeper UI / data-model context see brief.md.

About

thePHO bulk funding calculator — Te Whatu Ora revenue estimator and PHO pricing workbench. Tauri desktop app.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages