Audit-period reorganisation runbook¶
This is the staff-facing procedure for applying an organisational reorganisation (a trust merger, acquisition, dissolution, or ODS code change) to the Epilepsy12 audit-period organisation membership layer.
It is the operational counterpart to the design in audit-period-organisation.md. The design document explains why the model separates live current-state hierarchy from period-aware frozen hierarchy; this runbook explains how the audit team applies a reorganisation in production.
When to use this runbook¶
Use this runbook when:
- the
rcpch-nhs-organisationsAPI has been updated to reflect a merger, acquisition, dissolution, or ODS code change that affects one or more organisations with Epilepsy12 registrations; or - you are standing up the audit-period organisation membership layer for the first time in a new environment (staging or live); or
- you are re-syncing the in-flight cohort after a mid-cohort reorganisation has been reflected in the API.
Prerequisites¶
RCPCH_NHS_ORGANISATIONS_API_KEYis set in the environment.- The Django management commands are available:
sync_nhs_organisations,sync_audit_period_organisations,approve_audit_period_organisations. - You have an audit-team user account (RCPCH audit team member, RCPCH staff, or superuser) for the approval step.
- The API's temporal history layer is backfilled for the periods you intend to sync. See the pre-floor mergers section below for the limitation when it is not.
The two sync commands¶
There are two sync commands. They have distinct, non-overlapping responsibilities and must not be conflated:
| Command | Owns | Writes to | Trust it changes |
|---|---|---|---|
sync_audit_period_organisations |
Period-aware membership rows | AuditPeriodOrganisation |
Period memberships (AuditPeriodOrganisation.trust) |
sync_nhs_organisations |
Live current-state rows | Organisation, Trust, ICB, LHB, etc. |
Live Organisation.trust |
The per-cohort sync freezes the hierarchy as it was at each audit period's
reference date. The current-state sync moves the live Organisation.trust
FK to match the API's current state. The separation is what makes the
design safe: once cohort 7's trust assignment is frozen into
AuditPeriodOrganisation, a later current-state sync can move
Organisation.trust to the new trust without affecting cohort 7's KPIs —
because publication reads the frozen membership, not the live FK.
Cutover sequence (first run)¶
This is the step-by-step sequence for the first time you stand up the period-aware organisation membership layer in a new environment. It orders all five commands with what each does, why the order matters, and what to check before moving on.
Step 0 — Baseline review¶
python manage.py sync_nhs_organisations --dry-run
Preview what the current-state sync will do. Review the output for:
- Organisation trust/LHB moves and whether they carry registrations/cases exposure — these are the high-impact changes.
- Trust/LHB
activeflips (mostly dissolved/merged trusts goingactive=False). - Cases orphaned from no-cohort UI — the dry-run reports, for each
organisation going
active=Falsewith cases but no active successor linked viaOrganisationIdentity, the number of cases that would not appear in the no-cohort trust dropdown after cutover. These cases remain in the database and are reachable via cohort-selected views and direct organisation URLs, but they disappear from the no-cohort dropdown until--link-identitiesis run.
Pass --full to see every entry without truncation:
python manage.py sync_nhs_organisations --dry-run --full
Keep the dry-run output for the record.
Step 1 — Freeze historical memberships¶
python manage.py sync_audit_period_organisations
Populate AuditPeriodOrganisation rows for every audit period by calling
the API's snapshot endpoint at each period's reference date
(data_collection_end_date). This freezes, per period, which Trust/LHB/
ICB/region/network/country each participating organisation belonged to.
Rows are created unapproved (approved_at is null). This command never
touches live Organisation.* fields.
Step 2 — Review and approve the memberships¶
python manage.py approve_audit_period_organisations
Walk each unapproved row and decide whether the frozen period hierarchy is correct. The command shows the period hierarchy and the exposure (registrations and distinct cases attached to the organisation) so you can judge impact before committing.
At minimum you must approve memberships in the in-flight cohorts (currently recruiting / in data collection / in grace) to unblock the next step — the current-state sync's ordering constraint blocks on any in-flight period having zero approved memberships.
Flags:
--cohort N— scope to a single cohort.--ods-code CODE— scope to specific organisations (can be passed multiple times).--dry-run— review exposure without prompting or writing.--auto-approve— approve every unapproved row (review with--dry-runfirst).
See Pre-floor mergers for cases where the snapshot-sourced hierarchy is wrong and must be declined with a note.
Step 3 — Apply the live current state¶
python manage.py sync_nhs_organisations --confirm
This is the command that mutates the live Organisation.trust /
Organisation.local_health_board / etc. and flips active flags to match
the API's current state.
If the command prints Sync blocked by the ordering constraint: ...,
return to Step 2 and approve the in-flight cohorts before re-running.
Step 4 — Link OrganisationIdentity¶
python manage.py sync_audit_period_organisations --link-identities
Bridge ODS code changes from mergers/dissolutions by linking successor
Organisation rows to their predecessors via OrganisationIdentity. This
must run after Step 3 because the current-state sync creates the new
ODS code rows that this step links.
This step is what makes cases on an inactive predecessor organisation visible through the active successor in the no-cohort UI. Without it, the dry-run's "cases orphaned from no-cohort UI" warning would materialise: cases would remain in the database but not appear in the trust dropdown.
Step 5 — Confirm the changes¶
python manage.py sync_audit_period_organisations --reconcile
Produce a read-only verification report covering hierarchy changes between periods, registration attribution, and sibling organisations. This is the counterpart to Step 3 — it confirms the sync was applied as expected.
Summary of commands¶
| # | Command | Writes to | Trust it changes | Net effect |
|---|---|---|---|---|
| 0 | sync_nhs_organisations --dry-run |
nothing | — | Preview + orphan check |
| 1 | sync_audit_period_organisations |
AuditPeriodOrganisation |
period memberships | Freeze historical/period hierarchy (unapproved) |
| 2 | approve_audit_period_organisations |
AuditPeriodOrganisation.approved_at/by |
period memberships (locks) | Approve & freeze; unblocks Step 3 |
| 3 | sync_nhs_organisations --confirm |
live Organisation, Trust, etc. |
live Organisation.trust |
Move/reparent live organisations to current state |
| 4 | sync_audit_period_organisations --link-identities |
Organisation.identity / OrganisationIdentity |
— | Link ODS code successors to predecessors |
| 5 | sync_audit_period_organisations --reconcile |
nothing | — | Verify the whole cutover |
Pre-floor mergers¶
The rcpch-nhs-organisations API's temporal history layer started
recording on a specific date (the "floor date"). For audit period
reference dates before that floor date, the snapshot endpoint returns
the state as it was at the floor date — which may be the pre-merger
state if the merger predates the floor.
Worked examples¶
Ormskirk & District General Hospital (RVY02)
Southport & Ormskirk Hospital NHS Trust (RVY) was dissolved in 2013;
Ormskirk hospital then came under Mersey & West Lancashire Teaching
Hospitals NHS Trust (RBN). The API's temporal floor is in 2020, so the
snapshot endpoint returns trust RVY (the dissolved trust) for every
reference date in its window. The local E12 database has RVY02 under
RBN (the operationally correct parent).
When the per-cohort sync runs, it will create a candidate
AuditPeriodOrganisation row for RVY02 with trust = RVY for every
cohort — because that is what the snapshot returns. This is operationally
wrong: the cases were managed under RBN for every cohort after the 2013
merger.
Yeovil District Hospital (RA430)
Same pattern: Yeovil District Hospital NHS Foundation Trust (RA4) merged
into Somerset NHS Foundation Trust (RH5) in 2023. The snapshot returns
RA4 (the dissolved trust) for dates before the API's temporal floor. The
local database has RA430 under RH5 (the operationally correct parent).
What to do¶
When reviewing the per-cohort sync's candidate memberships in Step 2
(approve_audit_period_organisations), the audit team must:
-
Identify pre-floor cases. For each candidate row, compare the snapshot-sourced trust against the live
Organisation.trust. If they differ and the snapshot-sourced trust isactive=False(a dissolved trust), the row is likely a pre-floor case. -
Decline the row with a note. Use the
n(decline) option in the approval command and record a note such as "Pre-floor merger — operationally under<live trust>since<year>; snapshot returns<dissolved trust>because the merger predates the API temporal floor." -
Manually correct the membership if the period-aware reporting needs the operationally correct trust. The
AuditPeriodOrganisationrow can be edited directly (via Django admin or a data migration) to set the correct trust FK. Thesourcefield should be set to"manual"to record that the row was corrected by hand. -
Re-sync once the API backfills temporal history. If the API later backfills temporal history for the pre-floor period, re-running the per-cohort sync will update the unapproved (or manually-corrected) row. If the row was approved, the sync will not overwrite it — clear
approved_atfirst if you want the sync to pick up the new snapshot data.
The source field¶
Each AuditPeriodOrganisation row records its provenance via the source
field:
"snapshot"— the row came from the API's snapshot endpoint at the period's reference date."detail_fallback"— the snapshot returned 404 (no temporal history for this date), and the sync fell back to the detail endpoint (current state)."manual"— the row was created or corrected by hand.
Rows with source="detail_fallback" should be re-synced once the API
backfills temporal history for the relevant period.
Period-correct dropdown behaviour¶
After cutover, the operational dashboard and case list update their dropdowns based on the organisational relationships at the time of the selected cohort:
-
Cohort 7 selected (pre-floor reference date) — the dashboard shows the period-correct trust from the frozen
AuditPeriodOrganisationmembership. For Ormskirk this would beRVY(Southport & Ormskirk) if the snapshot-sourced membership was approved as-is, orRBN(Mersey & West Lancs) if the audit team corrected it to the operational parent. The period-context alert fires when the period-correct parent differs from the current parent. -
Cohort 9 selected (in-window reference date) — the dashboard shows the period-correct trust from the frozen membership, which for Ormskirk would be
RBN(the operationally correct parent, as returned by the snapshot for an in-window date). No alert fires if the period-correct parent matches the current parent. -
No cohort selected ("all children") — the dashboard uses the current live parent. No period alert fires.
The alert is contextual: it appears only when the period-correct parent and the current parent diverge, and it does not block or hide anything. It is the UX guard that makes period-correct dropdowns safe to ship.
In-flight cohort re-sync¶
The currently in-flight cohort (recruiting, in data collection, or in grace) is not yet historical. Its organisational hierarchy can still change while the cohort is open. Re-run the per-cohort sync periodically for the in-flight cohort:
python manage.py sync_audit_period_organisations --cohort <N>
The sync is idempotent:
- approved rows are never overwritten (an approved historical membership is stable);
- unapproved rows for the in-flight cohort are updated to the latest snapshot, which is the desired behaviour while the cohort is still open and the audit team have not yet approved the membership;
- once the cohort closes and the audit team approve the membership, the row is frozen.
Approving an in-flight membership freezes it against later
re-organisations for the remainder of the open cohort. Keep an in-flight
cohort's memberships unapproved until you are confident the hierarchy is
final, run the re-sync to pick up the latest snapshot, and only then
approve. If you approve too early and a mid-cohort reorganisation occurs,
clear approved_at on the affected row(s) before re-syncing.
Reference-date policy¶
The per-cohort sync uses AuditPeriod.data_collection_end_date as the
reference date for the snapshot call. This is the currently implemented
default.
An open decision for the audit team: should the reference date be
data_collection_end_date, recruitment_end_date, or
recruitment_start_date? The choice determines which cohort first picks
up a new trust after a merger:
| Policy | Reference date | A merger during cohort N's data collection takes effect from |
|---|---|---|
| A | recruitment_start_date |
cohort N+1 (first cohort whose recruitment opens after the merger) |
| B | recruitment_end_date |
cohort N (the cohort in recruitment at merger time) |
| C (current) | data_collection_end_date |
cohort N (the cohort in data collection at merger time) |
The reference date does not affect pre-floor mergers (the snapshot returns the same trust regardless of reference date). It only matters for mergers that land inside the API's temporal window.
This decision should be confirmed by the audit team before the sync runs.
If a different reference date is needed per period, an explicit
hierarchy_reference_date field can be added to AuditPeriod.
Rollback¶
- Steps 1–5 are safe to re-run. Step 2 only ever touches unapproved rows, so a re-run re-presents only what is still unapproved.
- Once Step 3 has run, re-running it is idempotent — it detects that live rows already match the API and reports no further high-impact changes.
- Rolling back a live trust move is not automatic. To undo a specific
move you would need to re-run a later current-state sync with the API
reflecting the previous state, or correct the
Organisation.trustmanually — there is no built-in undo in these commands.
Country invariant¶
England uses Trust as the reporting parent; Wales uses LocalHealthBoard.
The two are mutually exclusive for a given organisation. The per-cohort
sync and the current-state sync both preserve this invariant: an English
organisation's local_health_board must not be populated, and a Welsh
organisation's trust must not be populated. Reconciliation reports
Trust/LHB changes between periods but does not conflate the two.