Skip to content

Commit 66c04b0

Browse files
Merge pull request #5957 from nhsuk/unknown-la-csv
Add Unknown Local Authority fallback to reporting view
2 parents 3ad00e8 + bd2c3a2 commit 66c04b0

4 files changed

Lines changed: 157 additions & 3 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# frozen_string_literal: true
2+
3+
class UpdateReportingAPITotalsToVersion7 < ActiveRecord::Migration[8.1]
4+
def change
5+
update_view :reporting_api_totals,
6+
version: 7,
7+
revert_to_version: 6,
8+
materialized: true
9+
end
10+
end

db/schema.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
#
1111
# It's strongly recommended that you check this file into your version control system.
1212

13-
ActiveRecord::Schema[8.1].define(version: 2026_02_02_124516) do
13+
ActiveRecord::Schema[8.1].define(version: 2026_02_04_073325) do
1414
# These are extensions that must be enabled in order to support this database
1515
enable_extension "pg_catalog.plpgsql"
1616
enable_extension "pg_trgm"
@@ -1356,8 +1356,8 @@
13561356
ELSE NULL::text
13571357
END AS patient_gender,
13581358
((pps.academic_year - pat.birth_academic_year) - 5) AS patient_year_group,
1359-
COALESCE(la.mhclg_code, pat.local_authority_mhclg_code, ''::character varying) AS patient_local_authority_code,
1360-
COALESCE(la.official_name, pat_la.official_name, ''::character varying) AS patient_local_authority_official_name,
1359+
COALESCE(la.mhclg_code, pat.local_authority_mhclg_code, 'UNKNOWN'::character varying) AS patient_local_authority_code,
1360+
COALESCE(la.official_name, pat_la.official_name, 'Unknown Local Authority'::character varying) AS patient_local_authority_official_name,
13611361
COALESCE(la.mhclg_code, ''::character varying) AS patient_school_local_authority_code,
13621362
CASE
13631363
WHEN (school.urn IS NOT NULL) THEN school.urn
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
SELECT
2+
-- Composite key for unique index (required for concurrent refresh)
3+
pps.patient_id || '-' ||
4+
pps.programme_type || '-' ||
5+
tl.team_id || '-' ||
6+
pl.location_id || '-' ||
7+
pps.academic_year AS id,
8+
9+
-- Identifiers (for counting and grouping)
10+
pps.patient_id, -- COUNT(DISTINCT patient_id) for totals
11+
pps.academic_year, -- Filter: ?academic_year=2024
12+
pps.programme_type, -- Filter: ?programme=hpv
13+
pps.status, -- Scope: .vaccinated (status IN 60,61)
14+
tl.team_id, -- Filter: by user's team_ids
15+
pl.location_id AS session_location_id, -- Year group eligibility subquery
16+
17+
-- Patient demographics (used for filtering and CSV grouping)
18+
CASE pat.gender_code
19+
WHEN 0 THEN 'not known'
20+
WHEN 1 THEN 'male'
21+
WHEN 2 THEN 'female'
22+
WHEN 9 THEN 'not specified'
23+
ELSE NULL
24+
END AS patient_gender, -- Filter: ?gender=female
25+
26+
pps.academic_year
27+
- pat.birth_academic_year - 5 AS patient_year_group, -- Filter: ?year_group=8,9
28+
COALESCE(la.mhclg_code,
29+
pat.local_authority_mhclg_code,
30+
'UNKNOWN') AS patient_local_authority_code,
31+
COALESCE(la.official_name,
32+
pat_la.official_name,
33+
'Unknown Local Authority') AS patient_local_authority_official_name,
34+
COALESCE(la.mhclg_code, '') AS patient_school_local_authority_code,
35+
36+
-- School info (for CSV grouping by school)
37+
CASE
38+
WHEN school.urn IS NOT NULL THEN school.urn
39+
WHEN pat.home_educated = true THEN '999999'
40+
ELSE '888888'
41+
END AS patient_school_urn,
42+
CASE
43+
WHEN school.name IS NOT NULL THEN school.name
44+
WHEN pat.home_educated = true THEN 'Home-schooled'
45+
ELSE 'Unknown school'
46+
END AS patient_school_name,
47+
48+
-- Status flags
49+
ar.patient_id IS NOT NULL AS is_archived, -- Scope: .not_archived
50+
51+
-- Parent declared "already vaccinated" (counts toward vaccinated total)
52+
EXISTS (
53+
SELECT 1 FROM consents con
54+
WHERE con.patient_id = pps.patient_id
55+
AND con.programme_type = pps.programme_type
56+
AND con.academic_year = pps.academic_year
57+
AND con.invalidated_at IS NULL
58+
AND con.withdrawn_at IS NULL
59+
AND con.response = 1 -- refused
60+
AND con.reason_for_refusal = 1 -- already_vaccinated
61+
) AS has_already_vaccinated_consent
62+
63+
-- Source: pre-computed patient status per programme/year
64+
FROM patient_programme_statuses pps
65+
66+
-- Patient record (for demographics and exclusion checks)
67+
JOIN patients pat
68+
ON pat.id = pps.patient_id
69+
70+
-- Where the patient is enrolled this year (links to team via location)
71+
JOIN patient_locations pl
72+
ON pl.patient_id = pps.patient_id
73+
AND pl.academic_year = pps.academic_year
74+
75+
-- Which team owns this location (for team_id filtering)
76+
JOIN team_locations tl
77+
ON tl.location_id = pl.location_id
78+
AND tl.academic_year = pps.academic_year
79+
80+
-- Check if patient is archived by this team (LEFT: most aren't)
81+
LEFT JOIN archive_reasons ar
82+
ON ar.patient_id = pps.patient_id
83+
AND ar.team_id = tl.team_id
84+
85+
-- Patient's school (LEFT: home-educated patients have no school)
86+
LEFT JOIN locations school
87+
ON school.id = pat.school_id
88+
89+
-- School's local authority (LEFT: school may not have LA set)
90+
LEFT JOIN local_authorities la
91+
ON la.gias_code = school.gias_local_authority_code
92+
93+
-- Patient's direct local authority (LEFT: fallback when school has no LA)
94+
LEFT JOIN local_authorities pat_la
95+
ON pat_la.mhclg_code = pat.local_authority_mhclg_code
96+
97+
-- Exclude patients who shouldn't appear in any reports
98+
WHERE pat.invalidated_at IS NULL -- Merged/duplicate record
99+
AND pat.restricted_at IS NULL -- S31 restricted access
100+
AND pat.date_of_death IS NULL -- Deceased

spec/controllers/api/reporting/totals_controller_spec.rb

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,50 @@
339339
"Test Authority 202"
340340
)
341341
end
342+
343+
it "returns Unknown Local Authority for patients without school or postcode LA" do
344+
team = Team.last
345+
programme = Programme.hpv
346+
team.programmes << programme
347+
session = create(:session, team:, programmes: [programme])
348+
349+
# Patient with school that has LA
350+
school =
351+
create(:school, name: "Known School", gias_local_authority_code: 201)
352+
create(:patient, session:, school:)
353+
354+
# Home-educated patient without postcode LA lookup
355+
create(
356+
:patient,
357+
session:,
358+
school: nil,
359+
home_educated: true,
360+
local_authority_mhclg_code: nil
361+
)
362+
363+
# Unknown school patient without postcode LA lookup
364+
create(
365+
:patient,
366+
session:,
367+
school: nil,
368+
home_educated: false,
369+
local_authority_mhclg_code: nil
370+
)
371+
372+
refresh_reporting_views!
373+
374+
request.headers["Accept"] = "text/csv"
375+
get :index, params: { group: "local_authority" }, format: :csv
376+
377+
expect(response).to have_http_status(:ok)
378+
379+
csv = CSV.parse(response.body, headers: true)
380+
local_authorities = csv.map { it["Local Authority"] }
381+
expect(local_authorities).to contain_exactly(
382+
"Test Authority 201",
383+
"Unknown Local Authority"
384+
)
385+
end
342386
end
343387

344388
describe "Dashboard acceptance criteria" do

0 commit comments

Comments
 (0)