Skip to content

Fix silently-dropped OBR receipts targets (sheet 3.9 is APD, not current receipts)#439

Merged
vahid-ahmadi merged 3 commits into
mainfrom
fix/obr-receipts-sheet
Jul 19, 2026
Merged

Fix silently-dropped OBR receipts targets (sheet 3.9 is APD, not current receipts)#439
vahid-ahmadi merged 3 commits into
mainfrom
fix/obr-receipts-sheet

Conversation

@vahid-ahmadi

Copy link
Copy Markdown
Collaborator

Problem

_parse_receipts reads the cash-basis receipts rows from a hard-coded worksheet "3.9":

ws39 = wb["3.9"]
cash_rows = {
    "ni": ("National insurance contributions", "ni_employee"),
    "vat": ("Value added tax", "vat"),
    "fuel_duties": ("Fuel duties", "fuel_duty"),
    "capital_gains_tax": ("Capital gains tax", "capital_gains_tax"),
    "sdlt": ("Stamp duty land tax", "stamp_duty_land_tax"),
}

In the March 2026 EFO tables — the vintage currently configured in sources.yaml — sheet 3.8 is "Current receipts (on a cash basis)" and sheet 3.9 is "APD forecast - projection of passenger numbers". None of the five labels exist on 3.9, so every _find_row call raises ValueError, is caught, logs a warning, and appends no target.

The result is that all five cash-basis receipts targets are missing from the calibration surface — NICs, VAT, fuel duties, capital gains tax and SDLT — with nothing failing. Verified against the configured workbook:

Label Sheet 3.8 Sheet 3.9
National insurance contributions row 12 not found
Value added tax row 13 not found
Fuel duties row 21 not found
Capital gains tax row 22 not found
Stamp duty land tax row 24 not found

Note the existing cols_39/fy_39 column mapping (D→2024-25 … J→2030-31) already matches 3.8's layout exactly, which suggests the code was written against 3.8 and the sheet was renumbered by a later vintage without the reference being updated.

Fix

  1. Locate the worksheet by title ("current receipts" in cell B2) rather than by number, so a future renumbering cannot silently repeat this.
  2. Raise if none of the five rows are found. A per-row warning is right when one label changes; producing zero receipts targets is never a correct outcome and should not pass quietly.

Verification

Against the March 2026 receipts workbook the fix resolves sheet 3.8 and recovers all five rows (2026-27, £bn):

National insurance contributions   210.45
Value added tax                    187.70
Fuel duties                         24.63
Capital gains tax                   20.80
Stamp duty land tax                 18.15

Three unit tests cover the March 2026 layout, a hypothetical future renumbering, and the loud-failure path.

Why I noticed

Capital gains came up in a CGT reform analysis: the Enhanced FRS baseline produces ~1.29m CGT taxpayers holding £112bn of gains against HMRC's 378k and £66bn, and CGT revenue of ~£25bn against the OBR's £20.8bn forecast for 2026-27. Tracing why the OBR figure wasn't constraining the calibration led here. Restoring this target should pull CGT revenue toward the forecast directly.

Separately, populace has no capital gains targets at all in its UK surface — PolicyEngine/populace#459 — so gains totals and taxpayer counts remain unconstrained even with this fixed. The two are complementary: this PR restores the OBR revenue target; that issue is about HMRC gains and taxpayer targets.

The cash-basis receipts rows were read from a hard-coded sheet "3.9". In the
March 2026 EFO tables current receipts are on 3.8 and 3.9 is the APD forecast,
so every lookup raised ValueError, was caught, logged a warning, and produced
no target. All five cash-basis receipts targets - NICs, VAT, fuel duties,
capital gains tax and SDLT - were therefore missing from the calibration
surface with nothing failing.

Locate the sheet by its title so a future renumbering cannot repeat this, and
raise when none of the rows are found: silently dropping every receipts target
should never be a passing outcome.

Verified against the March 2026 receipts workbook, which now yields all five
(2026-27: NICs 210.45, VAT 187.70, fuel duties 24.63, CGT 20.80, SDLT 18.15
GBP bn).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two gaps in the first cut, both variants of the silent failure this PR
exists to prevent:

- The guard counted caught exceptions, so a vintage that shifted the value
  columns would find every label, produce no targets, and still pass. It
  now keys on rows that actually yielded values.
- Several sheets mention receipts ('Other receipts', 'Property transactions
  taxes: Receipts by sector'), and the matcher took the first substring hit.
  It now matches the full 'current receipts' phrase, prefers the cash-basis
  sheet when a vintage makes that ambiguous, and raises rather than guessing.

Also corrects the docstring, which still referred to Table 3.9.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant