Status: APPROVED by Matthew, 2026-09-23 — PHASE 1 IS READY TO BUILD. The plan is complete, the phase names at §6
are approved, and the recommendations at §7 are accepted (including D2, the phase 4 page correction, and D13,
the management view). Two items remain as human actions, neither blocking Phase 1: the TRUD account and its API
key (D12, needed before Phase 2) and CPE's consent to publish concession data (D10). The handoff note for
OpenCode is in from-hermes.
Direction as given: VeriPath takes the NHS release centrally, keeps it once, and makes it available to
every pharmacy client for their own information — uploaded from the backend of the central platform
application, not held on a client VPS. Licences are now settled for dm+d (Open Government Licence for TRUD) and
for the Drug Tariff (NHSBSA copyright, reusable under the Open Government Licence v3 with a prescribed attribution
statement), and are open for the price concessions, which need CPE's consent before they may be shown to clients —
the three are set side by side at 5.11. The requirement that a client can always see where the data came from is
a build requirement, specified at 5.6. What remains is a short list of decisions, each with a recommendation
— see §7. Phase 1 needs nothing from outside and can start as soon as the proposed phase names are approved (§6).
Where this sits. The capability is platform-level, not pharmacy-specific: the central application covers GP,
dental and pharmacy together (its current name is misleading and Matthew has deferred the naming — see open question
D7). The first consumer is the pharmacy app, which is why the page lives in this directory; if the capability
outgrows that, it wants a platform home of its own (D6).
Links: pharmacy app index ·
Supply Orders (§ Datasets, licences, and what this platform
will and will not hold) · Supply Orders Part 3 — pricing plan
· Phase 4 spec ·
SNOMED CT configuration (the precedent)
| # | Decided |
|---|---|
| 1 | The dm+d release is published weekly by the NHS, and VeriPath will update that data each week as the platform's own routine — not per client, and not by hand per pharmacy. |
| 2 | The data will be made available to all pharmacy clients, for their own information. |
| 3 | Therefore it must be stored centrally, not on a client VPS — one copy, maintained once. |
| 4 | It is uploaded from within the backend of the central platform application (today called the "GP Booking App", although it covers the whole infrastructure including dental; the name is misleading and will be dealt with separately). |
| 5 | Other data will follow the same route — uploaded centrally and made available to pharmacy clients. So this is a capability plan with dm+d as its first workload, not a dm+d-only page. |
| 6 | Client availability is a licensing answer, not a blocker: publishing the data to clients is acceptable so long as the source is clearly labelled, so that anyone looking at the data can see where it came from (Matthew, 2026-09-23). |
| 7 | The data is received by API wherever an API exists (Matthew, 2026-09-23, from his findings on NHSBSA Data) — so the routine is an automated pull, not a person downloading a file. 5.4 records which sources actually have one and which do not. |
The release is on disk and read by nothing. Held at /srv/datasets/dmd/2026-09-22 with its own MANIFEST.txt
(provenance, SHA-256 per file, licence). Every check below was run against the files themselves.
Completeness — the file set is the whole of what NHSBSA publishes for dm+d:
| Level | Rows | Note |
|---|---|---|
| VTM (virtual therapeutic moiety) | 3,250 | |
| VMP (virtual medicinal product) | 24,662 | |
| VMPP (virtual pack) | 37,397 | carries the Drug Tariff block (8,686 rows, 4,444 with a price) |
| AMP (actual medicinal product) | 167,307 | |
| AMPP (actual pack) | 187,156 | every pack carries a price row and a name (4,200 unnamed = the rows dm+d itself flags INVALID) |
| GTIN → pack mapping | 82,316 packs / 101,309 GTINs | 104,840 packs (56%) carry no GTIN at all |
| Ingredient substances | 4,532 |
DEMO- rows.The platform's documented SIAAS rule is that the central tier holds the organisation and routing metadata only,
and that the following must never live there (quoted from
the pharmacy overview): "patient-identifiable information, clinical
content, prescriptions or dispensing records, counter sales, stock, quoted prices, or any financial row".
This dataset is a different class of thing and the rule as written does not distinguish it:
What the plan proposes (needs Matthew's approval before any page is edited): add a carve-out sentence to the
central-tier rule so the distinction is explicit, in these terms —
Licensed national reference data — NHS terminologies and published tariffs that carry no client or patient
content and are identical for every tenant — is held once, centrally, as platform reference data. It is
versioned by release, licensed per release, and read by the tenant applications. A client's own prices,
supplier files, stock, quotes and commercial terms remain client-tier data and never move centrally.
What stays exactly as it is (no change proposed, and any future dataset must clear this bar): client stock and
valuations, wholesaler price files the client uploads, the client's PIP-coded catalogue, any patient-identifiable
information, any financial row of theirs. The existing ruling on
Supply Orders already refuses PIP-code redistribution and
vendor-held wholesaler credentials; this plan does not reopen either.
Licence position — answered in principle (Matthew, 2026-09-23): clear source labelling. The manifest's licence
line reads: "Internal platform use only — do not redistribute, publish or re-expose these files or derived
dictionaries to third parties." Making a derived dictionary available to each client pharmacy is the
"re-exposure" that line contemplates, and the ruling is that it is acceptable provided the source is clearly
labelled, so that anyone looking at the data can see where it came from. That condition is therefore a build
requirement in its own right — specified in 5.6, not a nicety to add later. Two things do not change: the raw
release files are never redistributed or published, and the licence register (5.5) records the terms for each
release as the evidence that the condition is honoured.
This is not a new kind of architecture for us. SNOMED CT is already a central reference dataset in the central
application's database (1.1 GB), loaded from the NHS RF2 distribution files and queried by the app:
| Table | Rows |
|---|---|
snomed_concepts |
638,855 |
snomed_descriptions |
1,956,244 |
snomed_relationships |
3,881,039 |
snomed_descendants (closure for hierarchy search) |
281,363 |
Two lessons to carry over rather than re-learn: (a) dm+d is the same class of artefact — NHS terminology,
versioned releases, no client content — so the storage-and-load argument is already settled in principle; (b) the
SNOMED load was done by hand from a zip placed on the server — precisely the weakness this plan set out to fix.
That weakness is now closed: the sixth source exists in code (reference_data: ReferenceSource.trud_item_id,
snomed_loader, import_snomed_release) and has been used twice on the live corpus — the 20260801 International
update (2026-10-16) and the UK Clinical Extension (2026-10-17), each staged, count-asserted per file, swapped in
one transaction and recorded as a release. The counts above are post-extension. The app also already has the upload pattern to copy: the backend data-import
dashboard (infrastructure/on-boarding/data-import/) does a preview-then-confirm, dry-run-first import with a
report of what it did with every row. Those importers are tenant clinical data, so reference data wants the same
shape, not the same code.
<AMPP> block — an AMPP block carries only APPID, NM, VPPID, APID,LEGAL_CATCD, SUBP; the prices sit in a sibling section (MEDICINAL_PRODUCT_PRICE → PRICE_INFO) keyed byPRICE_BASISCD (codes 0001–0004), translated by the lookupPRICE_BASIS table — the page says PRICE_BASIS; (3) the real filenames aref_ampp2_<release>.xml and friends, not f_ampp_v2.xml. The page also omits the file that carries thef_vmpp2). That page section should be corrected — proposed as part of thisThe SIAAS model means a pharmacy's application is served from the platform engine while its operational data
stays on its own box. Four ways to make central reference data available, in increasing order of duplication:
| Option | What it means | Trade-off |
|---|---|---|
| (i) Read-through | The tenant's application queries the central store at request time through the platform's existing connectivity. | Simplest, one copy, always current — but every reference lookup depends on the link being up. |
| (ii) Cached subset per tenant | Each tenant caches only the rows it uses (items it stocks, packs it orders), refreshed on release. | Small footprint, carries the "their box holds data" story, needs cache-fill and pruning rules. |
| (iii) Full copy per tenant | Push the whole published set into each tenant's own database on release. | Self-contained and offline-safe, but duplicates ~228 MB of licensed data per pharmacy and needs a real push/verify job. |
| (iv) Read-only API | The central tier exposes licensed endpoints; the tenant app calls them. | Clear entitlement and audit story — and the option most sensitive to L1 (is serving derived data to clients "re-exposure"?). |
The plan leans to (ii) over (i) — a read-through that fails is a pharmacy that cannot look up a product, whereas a
pruned local cache keeps working — but the decision waits on the labelling requirement (5.6) and on D1 (which
surfaces actually need it).
Updated 2026-09-23 — the leaning is now (i) for v1, because the objection has gone. That preference was formed
without checking where the store sits relative to the pharmacy application. Checked from inside pharmacy_app: its
default connection is the platform database that holds the store — 16 reference_data tables, the live dm+d
release and all 187,156 pack rows read straight from the pharmacy container. There is no new credential, no new
network path and no copy to make, and "the link may be down" does not apply while the application engine is ours. So
v1 of the client-facing view reads through (Data Feeds — Pharmacy View).
Caching (ii) stays the answer if a client ever runs without platform connectivity; a dedicated read-only database
role is named as later hardening, since today both applications share the connection.
The cadences differ, so the store needs a schedule and a release record per source, not one blanket "data update"
job. Every line below was checked against the source itself on 2026-09-23.
| Data | Cadence | Where it really comes from | Shape |
|---|---|---|---|
| dm+d — VTM / VMP / VMPP / AMP / AMPP, GTIN, ingredients | weekly | TRUD, under subscription, and it has an API: the release list is GET /trud/api/v1/keys/{key}/items/{itemId}/releases, then an authorised download of the weekly pack. Probed 2026-09-23 — the path answers with TRUD's own {"apiVersion":"1", …} envelope and a bad key returns 400 BAD REQUEST, so the endpoint is real. The dm+d item id must come from our own TRUD account, and nothing is fetched without the key. |
zip of XML — the release we hold is 8 files / 228 MB |
| Drug Tariff — the baseline reimbursement prices (Part VIIIA Categories A/M/C, VIIIB, VIIID, Part IX appliances, Part X) | monthly | NHSBSA, published as documents on the Drug Tariff pages — not on the Open Data Portal (see 5.7). The main parts are PDF; Part IX is also published as Excel ("Drug Tariff part IX September 2026 (Excel: 6MB)"). | PDF, plus XLSX for Part IX |
| Price concessions | monthly | Community Pharmacy England, monthly published list. A concession replaces the tariff price for the dispensing month and moves the product to discount not deducted. | an HTML table inside each monthly update post — no API, no spreadsheet, but parseable (5.8 C) |
| Discount deduction scale, Single Activity Fee | monthly | inside the Drug Tariff documents themselves. |
What the API route changes: dm+d becomes an automated pull — it is the only one of these sources with a real
API. The tariff and the concessions stay document fetch + parse, because no API exists for them. The TRUD key
lives in the platform secret store; the tariff and concession downloads are unauthenticated, so they need no
credential at all.
A missed release must be visible whichever route is used: the age of each live release is a dashboard figure,
and an alert fires when an expected publication date passes with no new release — weekly for dm+d, monthly for the
tariff and the concessions.
SNOMED CT UK Edition — the sixth source, and the reason this capability exists (2026-10-16). The GP app's
SNOMED corpus is held centrally already, but it was loaded by hand from a zip, it is the 2026-02 release,
it has never been updated, and no release identity for it is recorded anywhere. Matthew's direction: park the
corpus update until it has an update route of the kind dm+d has — and the route is this one: SNOMED CT UK
Edition is distributed by TRUD under the same account (our key is configured and subscribed to item 24,
dm+d, which pulls weekly), so the parked item is a second TRUD subscription plus a source entry and an
importer here, not new machinery. This is decision 5 above, arriving as the second workload.
DONE 2026-10-17 — the route exists and the corpus is current. snomed_uk_clinical (item 101) is a
subscribed source; the loader and the import_snomed_release command are deployed and were used for the
20260801 International update and then the UK Clinical Extension (20260923: +103,622 concepts /
+239,803 descriptions / +301,352 relationships). One gap to carry: the loader's editions union cannot
dedupe a revised component id — this release has 683 UK-revised relationships — so the extension had
to be merged into one file per role before loading, and the durable fix is a dedupe-on-id step in
load_release. Records: the SNOMED page and
/root/work/snomed-uk-ext-20261017/PROVENANCE.md.
Retention and rollback: keep the previous release importable per source, so a bad release can be
un-published without re-downloading; decide how many are kept (D4).
Storage: the raw files are disposable once parsed if the manifest and hashes are kept — a retention
decision, not an accident.
Until this is built, nothing changes: the 2026-09-22 release was fetched by hand and no credential is
held anywhere in the platform.
Matthew's ruling makes clear labelling the condition on which client availability is allowed, so it is specified
here rather than left to whoever builds the screen first.
£0.00 — only 92,294 of 187,156 packs carry a real price and 85,825 are "No Price Available".NHSBSA Data is where this plan's API route comes from. It is
right about the things that matter and wrong about three that a pipeline would be built on, so both halves are
recorded here. Checked against the sources themselves, 2026-09-23.
Update, 2026-09-23 (evening): the page's author-side model has since retracted or corrected every claim this
section corrects — see 5.10. Nothing below has changed; the reconciliation is recorded there.
Right, and relied on above: a TRUD REST API exists for the weekly dm+d pull; the Open Data Portal is
CKAN, so /api/3/action/… JSON calls really work (package_list, package_show and datastore_search all
answered); and the reimbursement chain is the right shape — a concession price replaces the tariff price for that
month, a concession item is discount-not-deducted, then the single activity fee, then a comparison against the
wholesaler's invoice cost.
Wrong — each of these would break a pipeline written to the page:
https://nhsbsa.net does not exist — no DNS record (checked 2026-09-23). The portal ishttps://opendata.nhsbsa.net, and a CKAN query needs the /api/3/action/datastore_search path; the page'senglish-prescribing-data-epd is titled "RETIRED -datastore_search cannotresource_id is a per-month identifier, not the page's placeholderepd_snomed_resource_uuid, and the page's example columns (SNOMED_CODE, DRUG_NAME, BASIC_PRICE,TARIFF_CATEGORY) are invented — a real resource's column list has to be read from package_show before aSame discipline as the guides. The Regulatory Content Audit
rule — name a regulatory artefact only when the app implements it and it is traceable to an authoritative source
with the source and version recorded — applies to this plan too. That is why 5.4 gives the cadence, publisher and
shape for every dataset, and why nothing here is taken from the research page unverified.
Verified against the sources themselves, 2026-09-23. "Have" means we already hold it or the source states it;
"missing" means nothing we hold answers it yet.
A. dm+d — weekly, TRUD API
| Required | State |
|---|---|
| A TRUD account | missing — nothing records one, and the licence text requires an open account to use the content. |
| A subscription to item 24 with the licence accepted | missing — and this is the gate. TRUD's own API guide: the endpoints are available to account holders "for items they are subscribed to", and the licence "must be accepted when you subscribe to this item". |
| The item number | have, supplied here — 24 = NHSBSA dm+d; 25 = NHSBSA dm+d supplementary (ATC classification and BNF codes for primary-care products — "not an officially endorsed dm+d product", status Supported). Item numbers are read from the releases-page URL, as TRUD's guide says. |
| The API key | Nothing to apply for — it comes from the account. TRUD issues no separate credential: the key is derived from the account's email address and password and shown on the account's Your account page (Matthew, 2026-09-23: "there is no API key" — correct, in that sense). What is needed is therefore the account itself, subscribed to the item, plus copying its key to the host. The trap stands: if the account's email or password changes, a new key is generated and the old one is disabled. |
| Endpoint, response shape, integrity data | have — GET /trud/api/v1/keys/{key}/items/24/releases (add ?latest for the newest); the JSON carries everything shown on the releases page plus extra details such as digital signatures — which the verify step should use — with releases newest-first. |
| Outbound access from the fetching host to TRUD | confirmed 2026-09-23 — from the app host, TRUD's landing page answers 302 and the API path answers its own {"apiVersion":"1", …} envelope. |
The licence of record for dm+d is better than we assumed. TRUD's licence page for item 24 names the
"Open Government Licence for TRUD": content "may be used under the terms and conditions of the Open Government
Licence", on three conditions — (a) an open TRUD account, (b) errors reported to the content provider, and
(c) "we recommend that you record how and where you use the content" (the licence register and import audit in
5.5 answer that). That is OGL-style reuse with attribution — precisely the "label it clearly so anyone can see where
it came from" condition Matthew set, and it is less restrictive than the line in the release's own
MANIFEST.txt, which reads "internal platform use only — do not redistribute, publish or re-expose…". That line
was written as a local conservative reading, not the licence's terms, and is superseded by the licence of
record (corrected on the file 2026-09-23).
B. Drug Tariff — monthly, documents, no API
| Required | State |
|---|---|
| Where each month's files are | have, with a warning — the Drug Tariff pages link the month's parts, but the file path carries the publication month, not the tariff month (the September 2026 files sit under /sites/default/files/2026-08/…), so URLs cannot be constructed. The page must be discovered each month, or the files uploaded by hand — decision needed. |
| Which parts we need | missing (a decision) — Part VIIIA Categories A/M/C, VIIIB, VIIID, Part IX, Part X, plus the fees and deduction sections. |
| A machine-readable format | partial — the main parts are PDF only; Part IX is also published as XLSX. So PDF parsing is in scope unless v1 is restricted to the machine-readable sets (dm+d's own Drug Tariff block, plus Part IX XLSX). Decision needed. |
| Reuse licence for the tariff | missing — no OGL or copyright statement appears on the pages fetched. Must be established with NHSBSA before it is published to clients. |
C. Price concessions — monthly, CPE
| Required | State |
|---|---|
| A machine-readable feed | none — but the list is parseable. CPE publishes each update as a news post whose body carries an HTML table of the month's concessions (product, quantity/pack, price), e.g. the 4th update of 22 September 2026 carried 52 rows (*Acamprosate 333mg gastro-resistant tablets |
| Reuse licence for CPE content | missing — not established. |
D. The one-line answer to "do we have everything we need?" For dm+d, one thing genuinely blocks the fetch —
a TRUD account with a subscription to item 24, the licence accepted, and the API key read from the account page;
everything else about dm+d is now known (item number, endpoint, response shape, integrity data, licence, and the held
release as the reference to test against). The two monthly sources need decisions rather than credentials (how
each month's documents are discovered; whether PDF parsing is acceptable), and their reuse licences are the real
open question — dm+d's Open Government Licence does not cover them.
The foot of NHSBSA Data carries a block addressed to us as an
instruction: "build an automated ETL pipeline that streams open reference data from the NHSBSA Open Data Portal into
a centralized database without using an API key." It must not be built as written — it restates the three errors
in 5.7 in imperative form, and adds a fourth:
https://nhsbsa.net has no DNS record, so every request built to it fails.https://opendata.nhsbsa.net and the path is /api/3/action/datastore_search.EPD_SNOMED_202011) returns 17,338,724 rows — not the block's "145000" — and its actual columns areYEAR_MONTH, BNF_CODE, BNF_DESCRIPTION, CHEMICAL_SUBSTANCE_BNF_DESCR, QUANTITY, ITEMS, NIC,ACTUAL_COST, SNOMED_CODE and practice/address fields. There is no BASIC_PRICE and no TARIFF_CATEGORY;What the block gets right, and what we adopt from it (good engineering, independent of its wrong target):
resource_id / limit / offset with a records, fields, total envelope —Risk to flag: the block reads as an instruction to whichever agent opens that page next. Until the page carries a
dated correction, a build from it would target a dead host with invented columns — and would still not contain a
single Drug Tariff price.
Matthew put this plan's request for evidence to the research page's author-side model (Gemini) and added the reply to
NHSBSA Data under "Hermes request to Gemini". Every
substantive claim in that page's instruction block has now been retracted or corrected by the model that made it:
| Item | What the page claimed | The model's verdict, 2026-09-23 | This plan's position |
|---|---|---|---|
| Base endpoint | https://nhsbsa.net |
Retracted in part — "an erroneous shorthand"; the working domain is opendata.nhsbsa.net |
Unchanged — the host does not resolve |
| Tariff on the portal | Part VIIIA prices queryable by dataset or resource_id |
Retracted — the portal does not host a machine-readable dataset of monthly Part VIIIA reimbursement prices | Unchanged |
| Response schema | SNOMED_CODE, DRUG_NAME, BASIC_PRICE, TARIFF_CATEGORY |
Retracted — "an idealized code blueprint mapping fields from disparate sources", not an endpoint payload | Unchanged |
| dm+d without a key | Retrievable with no account, subscription or key | Retracted — TRUD "strictly require[s] an active account, a standard item subscription, and a valid account API key" | Unchanged — 5.8 lists what is needed |
| Concessions feed | A machine-readable API or database feed | Retracted — no feed or API; the HTML must be parsed | Agreed — with the verified detail at 5.8 C |
| Tariff format | Part VIIIA available as a spreadsheet or database | Retracted — PDF only | Unchanged |
Two things worth recording about the exchange, because they change how that page should be read — not this plan:
getent, a resolvercurl, which fails with Could not resolve host; while opendata.nhsbsa.net resolves normally). Item 4One improvement its answer prompted. Asking about the concession feed made me test it rather than assume: the list
is machine-parseable — each monthly update post carries an HTML table of the month's concessions, verified above.
That removes one of the two decisions at 5.8 C (the other, the reuse licence, stands).
Three sources, three different licences. This is the table the licence register (5.5) starts from, and the reason the
labelling requirement (5.6) is a licence condition rather than a courtesy.
| Source | Licence, as published | What it permits | What must accompany the data |
|---|---|---|---|
| dm+d — TRUD item 24 | Open Government Licence for TRUD (TRUD's item licence page) | Use under the terms of the Open Government Licence, on conditions: an open TRUD account; errors reported to the content provider; a record kept of how and where the content is used | Source NHSBSA dm+d, distributed by TRUD, with the live release identity and date |
| Drug Tariff — NHSBSA documents | NHSBSA copyright, reusable under the Open Government Licence v3 (NHSBSA's terms and conditions) | Download and re-use in accordance with OGL v3, provided the prescribed statement is included; NHSBSA accepts no liability for the use made of the information and does not warrant uninterrupted supply | The prescribed statement, in their words: "[NHSBSA Title of Information], NHSBSA Copyright [current calendar year]" with the OGL reference — plus the tariff month |
| Price concessions — CPE | PSNC/CPE copyright (CPE's website terms) | Copying documents or pages is authorised "for personal use only", retaining the copyright notices, and "no documents or resources from the website may be uploaded to other websites without the prior consent" of PSNC. Hyperlinks need no consent | If consent is obtained: CPE attribution and the month. If not: the concession data is not shown to clients at all |
Why this settles the licensing question properly. Matthew's ruling — publish it, provided the source is clear —
is sufficient for dm+d and for the Drug Tariff, because both are licensed for re-use with attribution; the tariff
even dictates the wording. It is not sufficient for the concessions, where the terms reserve re-publication to
PSNC and require prior consent: reading and parsing them for the platform's own purposes is one question, and showing
them to client pharmacies is another. Asking CPE for consent is now a named action (D10), not an assumption.
Recommended: build it in Phase 1, as part of this capability — not as a bolt-on after the other phases. Matthew's
steer, 2026-09-23: this aspect of the app is managed and controlled from a view in the platform backend
(https://gp.veripath.co.uk/), whose "gp" name is misleading because that backend now serves every app — the same
deferral as D7. Two reasons to add it now rather than later: Phase 1's acceptance criteria already include publishing
a release, which implies a control surface; and the licensing and consent rules (5.11) need somewhere to live where
they can actually be enforced. Retrofitting it after phases 2–5 would reopen criteria already accepted.
Why the backend's existing infrastructure area, rather than something new. The pattern already exists there and
needs no invention:
| Existing pattern | Where it already is | What this capability reuses |
|---|---|---|
| An infrastructure area with its own sub-navigation | the backend's /infrastructure/ pages and their _nav.html partial |
one new entry alongside the existing pages, not a new top-level area |
| Platform-admin-only access | PlatformAdminRequiredMixin, already on every infrastructure view |
the same guard: uploading and publishing is a platform act (D5) |
| A preview-then-confirm import screen | the on-boarding data-import dashboard | the interaction shape for verify → preview → confirm (5.2) |
| A superuser "fleet view" distinction | the infrastructure dashboard | room to separate "may see everything" from "may operate" |
What the view must show and do (first cut, to be settled when Phase 1 is specified):
What it is not. Not a client-facing screen: no pharmacy tenant sees it, and no tenant account can upload, publish
or roll back (D5). A client sees the published data with its label (5.6), never the machinery.
Deferred with the app's name (D7). Where the view sits in the backend's own navigation, and what that backend is
called, are the questions Matthew has parked. It can be built in its current home and renamed with the rest.
Part 1 of his 2026-09-23 steer now has its own spec — the deliberately minimal answer to "so long as I can see
everything is basically running and up-to-date": Data Feeds — Backend Status View.
It is read-only, adds no new controls, and exists because the surface today reports a TRUD key problem against
feeds that use no key and have no routine (Drug Tariff, Price Concessions) — two of its three alerts are noise that
reads as a fault. The pharmacy-side display that follows it is Part 2 (D1).
The sequence is now build-ready: each phase states what it needs, and Phase 1 needs nothing from outside.
Where this is going — Matthew's steer, 2026-09-27. Collecting these datasets is not the point in itself: the
concession figures must work with the dm+d (TRUD) data and the reimbursement (Drug Tariff) data, and the
combined pricing they produce is what a client compares against their own supplier pricing imported into the app.
He describes what is done so far as obtaining the required sources; the next stages are to see this data and
then to ensure it is processed so that it serves that purpose — with depth and breadth still to come. In one line:
dm+d + tariff + concessions → the derived pricing per pack → compared with the client's own supplier prices.
Phases 6–8 below are that work. Its first mechanic is the pack matching (the concession rows carry no code of any
kind — §18 item 2 of the phase 5 spec), because
nothing can be aligned until a concession line is tied to a pack.
| Phase | Scope | Needs | Acceptance criteria |
|---|---|---|---|
| 1. The store and the loader | The central store (5.1), the release model, the backend upload screen with verify → preview → confirm → publish (5.2), the backend management view that controls it (5.12), then the held 2026-09-22 release imported as the first real release | Nothing external — the release is already on disk with its manifest | ✅ DEPLOYED AND VERIFIED LIVE, 2026-09-23 (438e565 on the app host): the held release is imported, published and live as 3170926; reference_data_check passes — integrity re-proved against the manifest, 10 closure checks 0 dangling, 0 packs without a price row, 0 prices stored as zero; a re-import is a no-op; the consent gate refuses to publish the concession source; the view area is platform-admin only (403 anonymously). Full record: §10 |
| 2. The release routine | The scheduled TRUD API pull, freshness monitoring, retention and rollback (5.4) | The TRUD account, its subscription to item 24 accepted, and the API key (D12); plus the egress check | ✅ BUILT (2301095) AND DEPLOYED 2026-09-23: the health surface is live (live release age, next expected publication, key status not_configured, alerts), the purge keeps the live release, and the beat schedules are registered. The live TRUD call is the one thing still unverified — it needs the account (D12). See §10 |
| 3. Client availability | The chosen surface (D1) reads the published release, with the 5.6 labelling generated from the release record | Phases 1–2; D1 | The label is present on every surface and export, naming the live release — asserted by a checker with the release identity, not eyeballed |
| 4. The Drug Tariff | Monthly tariff ingestion — fetch-and-parse (D11 answered 2026-09-26) | Nothing external — no account, no key; egress to the NHSBSA pages confirmed from inside the app container (200) | ✅ DEPLOYED AND VERIFIED LIVE 2026-09-26 (c312557, migrations 0006+0007): release 2026-09 published unattended in 27 s (9.85 / 20 / 5 %, 152p); verifiers 49/49 and 16/16; suite 108 OK; the gate refuses five doctored documents and a doctored month holds. The scheduled-dispatch caveat found on the way is FIXED (5bf0857 — the ambiguous redis hostname) and re-proven: the queue drains, beat dispatches, zero auth errors. See §12 |
| 5. Price concessions | Monthly concession ingestion from CPE's email alert stream (the values in the message body) | Nothing external — the alerts already arrive in the mailbox and the credentials are in the app host's environment. Reaching client screens needs the licensing basis recorded — Matthew's task (phase 5 §15) | ✅ BUILT AND VERIFIED LIVE 2026-09-27 (8abc4bd, migration 0008): the real mailbox ingested unattended — 5th update 182 = 182, 6th 216 = 216 — release 2026-09 imported and never live (the gate holds); live verifier 37/37; suite 136 OK (1 skipped); the same-day cross-check against CPE's own page matched exactly (216 rows, 0 missing, 0 extra, 0 prices differing). See phase 5 spec §17 |
| 6. See the data | The combined view: per pack, the dm+d (TRUD) price, the reimbursement position from the tariff parameters, and the concession override with its zero-discount effect — the store's output shown together so it can be judged before it is relied on. Prerequisite: the pack matching (phase 5 §18 item 2) | Nothing external — all three sources are live | The three sources are visible side by side for a chosen month, and each figure traces to its own release and published document. ✅ BUILT AND VERIFIED LIVE 2026-09-27 — pharmacy feature/pack-link @ e257181 (platform 0907677): the month is 216/216 matched, queue empty, verifier 23/23, suites 182 / 33 / 176, and the client surfaces 31/31 over HTTPS; the §9.1 wording split is ruled and briefed. Spec: Data Feeds — the Pack Link |
| 7. Process it — the derived pricing | The reimbursement arithmetic over the store: the tariff price or the concession price, the deduction scale, the Single Activity Fee, and the discount-not-deducted rule that applies to a line on concession. This is the guardrail that stage 4 of the pricing plan postponed | Phases 1–6; its own spec and its own accuracy argument — this is a calculation layer, not a feed | The derived figure for a pack is reproducible from the store, and an accuracy cross-check against the Open Data Portal's zero-discount/standard-rate split (D9) is stated rather than assumed. ✅ BUILT AND VERIFIED LIVE 2026-09-27 (platform 1c6f9a3, pharmacy 78c9b2c): verifier 26/26, all 4,439 |
figures recomputed independently, suites 209 / 35 / 178, client surfaces 22/22; one bounded gap measured
— Part II's group rules are stored but not applied, touching 112 packs of 4,439 (2.5%), awaiting Matthew's call
(spec §9.1): The Derived Pricing — Phase 7 — the Part II "drugs for which discount is not deducted" rule is the piece v1 of the tariff ingestion deliberately did not parse (1,463 items, 98.4% matched by two deterministic rules, residue 1.6% named), and D9 is corrected there: the portal carries no tariff and no rates (plan §13), so it cannot cross-check the arithmetic — a bounds check on implied discount is recorded as a later strengthening instead |
| 8. The client comparison | The derived pricing (phase 7) set against the client's own supplier pricing, already imported (pricing stages 1–4), so a contractor can compare what they pay with what the NHS reimburses | Phases 1–7; the client-facing surface (D1) and the licensing basis | A contractor sees, for their own packs, their purchase price beside the derived reimbursement — every figure carrying its source label and release identity. ✅ SPEC WRITTEN AND AUTHORISED 2026-09-27, measured before it was written: the bridge does not exist (a StockItem has no dm+d reference), PIP cannot reach dm+d (the store holds no PIP attribute — only GTIN), 3,779 of the 4,439 priced packs (85.1%) are reachable from a live GTIN and 660 are not, and the test catalogue bridges 0 of 9 items. Rulings taken under Matthew's delegation of 2026-09-27: GTIN first / name+pack second / the rest queued with a manual override; the comparison on the screens that already show a supplier price; the concession figure may be named to clients because the live rows cite the NHSBSA notices (OGL v3, no consent gate) and not the gated CPE stream; the demo seed gains real dm+d GTINs. ✅ BUILT AND VERIFIED LIVE 2026-09-27 — pharmacy feature/phase8-client-comparison @ 49bd358, migration pharmacy_stock/0008: the bridge resolves 7 of 13 catalogue rows by GTIN and queues the other 6 with their reason, all 7 figures recompute equal to the stored rows, the resolver is idempotent, the store is untouched, the client surfaces are 64/64 over HTTPS, and the comparison is structurally advisory (the ordering path does not read it at all). Spec §11 carries the evidence. Spec: The Client Comparison — Phase 8 |
On the guardrail, which was a downstream candidate when this plan was written (2026-09-23): the reimbursement guardrail that stage 4 of the pricing plan postponed — the buy-above-reimbursement comparison, the concession→zero-discount rule and the Single Activity Fee — is now phase 7, because Matthew's steer of 2026-09-27 makes the derived pricing the purpose of the whole programme rather than a later possibility. It remains a calculation layer over this store, with its own spec and its own accuracy argument; what has changed is that it is now the destination rather than a candidate. The two components the flag was waiting on are both live: the tariff parameters (phase 4) and the concessions (phase 5).
Phase 8 was specified and authorised on 2026-09-27 and is now BUILT AND VERIFIED LIVE the same day — The Client Comparison — Phase 8. It is the stage the guardrail flag was parked for: the phase-7 figure set beside the contractor's own imported purchase price, on the screens that already show a price (the low-stock report, the expiry report, the order build), advisory only and never blocking an order. Its one missing piece was the bridge from a StockItem to a dm+d pack, which was measured before it was specified: the store holds no PIP attribute of any kind, so PIP matches a price to an item and can never reach a pack; the coded route reaches 3,779 of the 4,439 packs carrying a figure (85.1%) from a live GTIN, with 660 unreachable that way; and the name route needs the item's free-text pack size resolved to the VMPP (a VMP holds several packs — Amoxicillin 500mg capsules has 6, only 2 priced). Both GTIN widths matter (74,016 rows at 13 digits, 10,404 at 14), so a one-shape match would lose packs silently. Two of the phase's decisions were taken by Hermes under Matthew's delegation of the same day and are recorded on the spec with their reasons, reviewable: the client-facing figure may name a concession-derived reimbursement — the live rows cite the NHSBSA Drug Tariff News notices (OGL v3, consent_required = false, release published and live), not the gated CPE stream (consent required, not granted, release imported and not live) — and the demo seed gains real public dm+d GTINs so a walkthrough can show the comparison working, with two items deliberately left unbridgeable to show the queue. The build is live and verified: 7 of the 13 catalogue rows bridge by GTIN and the other six queue with their reason, all seven figures recompute equal to the stored rows, the resolver re-run is a no-op, the central store is untouched, the client surfaces pass 64/64 over HTTPS through the real OIDC login, and the comparison cannot block an order because the ordering path never reads it (spec §11).
Already answered, so they are off the list: L1 — client publication is allowed with clear source labelling
(5.6, and now backed by the licence text at 5.11); D3 — the fetch route is a TRUD API pull with the key in the
platform secret store; D8 — the datasets are dm+d (weekly), the Drug Tariff (monthly) and price concessions
(monthly), with the ODP activity data excluded for now; D10's tariff half — NHSBSA content re-uses under OGL v3
with their prescribed statement; D11's concession half — the monthly table is parseable.
| # | Decision | Recommendation | Why it matters |
|---|---|---|---|
| D1 | Which pharmacy surface shows the data first | ANSWERED 2026-09-23 (Matthew's Part 2 steer): a read-only "product information" view in the pharmacy application — v1 searches by name and shows the pack with its published price or "no published price"; SNOMED-code and barcode lookup follow with the first modification. Spec: Data Feeds — Pharmacy View | Lowest risk, no commercial implication, and it exercises both the labelling requirement and the caching decision (5.3) |
| D2 | Correct the phase 4 page's "Trud - all products" section to the real file layout | Yes — as a dated correction, not a silent edit | It is wrong today and will mislead whoever writes the loader |
| D4 | Retention: how many releases, and are raw files kept? | Keep the current and previous release importable per source; keep manifest + hashes indefinitely, raw XML only for the latest | Rollback without a re-download; the manifest is the audit evidence |
| D5 | Who may upload and publish a release | Platform admin only — no client-facing upload screen | It is a central act, and it is the licence register's point of record |
| D6 | Page placement once a second sector consumes this | Keep it in the pharmacy directory until a second consumer arrives — the same trigger as D7 | No churn while pharmacy is the only consumer |
| D9 | Ingest the ODP's activity datasets (EPD with SNOMED Code, PCA)? | Out of scope for phases 1–5; decide separately if a use is named | They answer what was dispensed and what it cost, not what will be paid — and one month's resource alone holds 17.3 million rows |
| D10 | CPE consent to publish price concessions to clients | SUPERSEDED TWICE — 2026-09-26/27. No request is sent to CPE (Matthew, 2026-09-26), so the recorded terms stand and the code gate keeps the source off client screens. Then, 2026-09-27: the concession data is to be made available to clients and Matthew owns the necessary licensing tasks — the gate opens by that basis being recorded on the licence register (consent_granted, or consent_required=False with a reason and a date), never by code drift. Detail: phase 5 spec §15 |
Until the basis is recorded the display stays closed by design, so nothing leaks in the interval |
| D11 | Tariff: how each month's documents are found, and whether PDF parsing is acceptable | ANSWERED 2026-09-26 (Matthew): fetch-and-parse. The NHSBSA hub page is read each month and the documents fetched and parsed — no manual upload. v1 parses four parameters only (the three deduction rates and the Single Activity Fee). Publishing is unattended when a validation gate passes, and holds with an alert when it does not. Spec: The Drug Tariff — Phase 4 Ingestion | Demand-driven lookup may need far less than full coverage; dm+d's block holds 4,444 priced tariff rows (Cat A/C/M, VIIIB and Part IXa) |
| D12 | The TRUD account the weekly pull runs under: a VeriPath organisational account, subscription to item 24 accepted, and its key copied into the platform secret store | Settle it before Phase 2, not before Phase 1 — and give it a named owner. No key has to be applied for (it is derived from the account and shown on its account page); the gate is the account and its item subscription | TRUD's API is available "to account holders for items they are subscribed to"; the derived key dies silently if the account's email or password changes |
| D14 | How the app host is deployed | APPROVED by Matthew 2026-09-23: make the app host a git checkout and deploy as git fetch && git checkout <commit> → docker compose build app celery_worker celery_beat → up -d, with config/secrets outside version control |
The host currently has no defined deploy path (not a checkout, a stale scripts/deploy.sh, and code baked into the image, so a restart would not pick a change up) |
| D13 | Where the management view lives in the backend, and who may reach it | Build it inside the existing infrastructure area of the backend, behind PlatformAdminRequiredMixin, adding one entry to that area's sub-navigation — platform admins only, never a tenant |
It is the control surface for upload, publish and rollback, and the home of the licence register and consent flags (5.12): worth agreeing before Phase 1 builds it |
| D7 | The central application's name | Deferred by Matthew — recorded so it is not lost | — |
Run against /srv/datasets/dmd/2026-09-22 (release 3170926, prices dated to 17/09/2026):
MANIFEST.txt SHA-256 and byte size (f_ampp2 → 883ad186…3709, 120,616,956INVALID (retired products kept as id stubs, still linked and still priced).grep/sort/comm over extracted ids takes 25 seconds. Use the cheap method for the loader's tests too.Built by OpenCode (gp_booking_app, branch feature/central-reference-data-phase1, commit 766a46e, pushed — not
deployed). Verified by Hermes without deploying it, in a worktree on VPS1: no production change was made.
What was run, and what it returned
REFDATA_DMD_PATH=/srv/datasets/dmd/2026-09-22 scripts/run_reference_data_tests.sh — 33 tests OK in 80s,reference_data_check ran twice in that suite and passed both times: integrity re-proved against the manifest,pence() maps an empty, non-numeric or zeroReferenceSource.consent_required / consent_granted / can_publish, with the/infrastructure/reference-data/, included from the infrastructurePlatformAdminRequiredMixin.302 and the API path answers its{"apiVersion":"1", …} envelope.Why it is not deployed yet — two separate blockers
***REMOVED*** in appointments/views.py (both NHSNotifyService(api_key=…) calls — and thedsp_clinic/settings.py (the CHANNEL_LAYERS fallback, with noCHANNEL_LAYER_URL in the env, so the fallback is what runs) and tenancy/migrations/0002 (seeded db_password).scripts/deploy.sh is stale — it/var/www/html, git pull origin main and a container restart, while the code is baked into the image,git fetch && git checkout <commit> → docker compose build app celery_worker celery_beat → up -d.Flagged, not fixed: the deployed tree and the image built from it still contain the pre-redaction plaintext
secrets the repository has since cleaned — the platform's known open credential exposure. The first proper deploy is
the moment to close it rather than carry it into another image.
Where this leaves Phase 2: the account side (D12 — a TRUD account, subscription to item 24 accepted, API key) is
Matthew's; the parts that do not need it — the freshness/health surface, retention and rollback, and detection of a
dead key — can be built now.
Both phases are deployed and verified on the live platform. Phase 1 (438e565) was landed first and verified on
its own, then Phase 2 (2301095).
172.18.0.16:2222 refuses), the pre-existing tree was backed up first, and theconfig/secrets was never touched and no secret value passed through theCHANNEL_LAYER_URL and NHS_NOTIFY_API_KEY —/app (with media and staticfiles too), so amigrate and collectstatic on start, so the restart appliedreference_data.0001–0004 and collected static files. migrate --plan was checked before the first restart andVerified 8 files, 238,847,891 bytes, release 3170926 → Imported release 3170926: 752,256 rows across 8 files →Release 3170926 is now live.reference_data_check --path … → "All checks passed." with the counts matching §2 exactly (VTM 3,250 · VMPdmd has 1 release, 1 live; the other two sources 0.imported and published events, and a licence snapshot naming Open Government/infrastructure/reference-data/ and …/upload/ return 403; /infrastructure/health/ still returnspublication_date null (the--publication-date), which left the freshness figuresnhsbsa_dmd_9.2.0_20260921000001.zip). The held content was generated on 17/09/2026, so that is the othermedia/reference_data/raw/dmd/3170926.zip (16 MB);0004 applied and the routes are wired: /infrastructure/reference-data/ → release list,…/health/ → the freshness page, …/upload/ → the upload screen — all behind the platform-admin guard (4033170926, age 2 days, next expectedoverdue False, and key status not_configured with the alert "No API keyreference_data_purge → "0 release(s) purged; manifests and hashes retained" — the liveMANIFEST.txt?--publication-date or derive it, so freshness is never silently blank.ReferenceFetchAttempt, so the attempt log is empty for the "not configured" case. The health surface still reports⚠️ Nit 2 was misreported — see the redeploy record below. fetch_latest_release creates the attempt at the top and
its finally block saves it with finished_at; the live store carried a not_configured row before the fix as
well as after. The reported gap did not exist.
82871c4 (both fixes, 61 tests OK) was redeployed to the app host the same evening:
/root/work/gp_booking_app fast-forwarded 2301095 → 82871c4 via a git bundle (Forgejo remains unreachable from
that host), the three containers restarted (repo bind-mounted at /app, entrypoint runs migrate + collectstatic),
gp_booking_app back healthy, platform health 200.
derive_publication_date on TRUD's own archive namenhsbsa_dmd_9.2.0_20260921000001.zip) → 2026-09-21 — the date entered by hand in the phase 1 record below, now3170926, publication date 2026-09-21, age 2 days, next expectedoverdue False./infrastructure/reference-data/, …/health/, …/upload/ → 403 to anonymous; theMANIFEST.txtThe question, Matthew 2026-09-25: the reimbursement data may be in the zip the release was imported from — check it, and if
it is not there, establish where the source is. Answer: the delivery is audited, and the reimbursement parameters are
not in it and cannot be — but the reimbursement prices are, and the audit also found three members that were dropped at
import.
Audited on Matthew's own machine (Ubuntu, unzip) against his original download, output pasted back:
~/Desktop/nhsbsa_dmd_9.2.0_20260921000001.zip — 16,880,148 bytes, 16 members, 228,484,358 bytes uncompressed.
The copy on the app host (media/reference_data/raw/dmd/3170926.zip) is 16,356,672 bytes — 523,476 fewer.
| Member(s) | Bytes | In the store? |
|---|---|---|
f_amp2_3170926.xml, f_ampp2_3170926.xml, f_ingredient2_3170926.xml, f_lookup2_3170926.xml, f_vmp2_3170926.xml, f_vmpp2_3170926.xml, f_vtm2_3170926.xml |
76,529,587 / 120,616,956 / 417,944 / 271,751 / 20,472,872 / 8,184,385 / 363,674 | Yes — imported; every one is the same byte size as the file held, so the data we hold is this download |
week392026-r2_3-GTIN.zip (nested) |
1,303,821 | Yes, expanded — the GTIN data is not a member of the archive: it arrives as its own nested zip, and we hold it as f_gtin2_0170926.xml (11,990,722 bytes, sha256 caaa16d8…, 101,309 GTIN rows live). The filename we hold should be reconciled against the nested zip's own member name — hash check requested, pending |
amp_v2_3.xsd, ampp_v2_3.xsd, ingredient_v2_3.xsd, lookup_v2_3.xsd, vmp_v2_3.xsd, vmpp_v2_3.xsd, vtm_v2_3.xsd |
35,461 total (dated 2010–2013 — schema v2.3 unchanged) | No — dropped at import |
docs/dmd_extract_comms_9_2_0.pdf |
287,907 | No — the dm+d v9.2.0 release communications, unread |
MANIFEST.txt (in the imported set) |
2,996 | ours, written for the review — not TRUD's |
The 523,476-byte difference between the two zips is therefore explained: the seven XML members are byte-identical in size
and the difference is the members left out (35 KB of XSDs, 288 KB of comms PDF) plus the GTIN member kept as its own 1.3 MB
zip rather than the ~12 MB expanded XML we hold.
The tariff search — whole archive, then member by member:
deduction, discount scale, activity fee, single activity fee: 0 occurrences anywhere in the 16 members.DTINFO occurs in f_vmpp2 only (named in vmpp_v2_3.xsd): the per-pack Drug Tariff block — pay category, price,Category M (×1) and Part VIIIA (×4) in f_lookup2's DT_PAYMENT_CATEGORYConclusion. The reimbursement prices for the mainstream categories are ours already, inside dm+d's own block, and
refresh with every release. The reimbursement parameters — the discount deduction scale and the Single Activity
Fee — are not in the TRUD delivery and never were; nor are the un-priced appliance parts. They exist only in the monthly
NHSBSA Drug Tariff documents, so the source hunt points at the Drug Tariff pages, not at TRUD — and the open item
stays D11 (how each month's files are discovered; whether PDF parsing is acceptable).
Live coverage of the block (measured in dsp_clinic, 2026-09-25):
| Pay category | Rows | Priced | Newest tariff date |
|---|---|---|---|
| Part VIIIA Category A | 489 | 489 | 2026-07-01 |
| Part VIIIA Category C | 2,433 | 2,432 | 2026-09-01 |
| Part VIIIA Category M | 624 | 624 | 2026-07-01 |
| Part VIIIA Category H | 19 | 19 | 2026-09-01 |
| Part VIIIB | 474 | 474 | 2026-08-01 |
| Part VIIIC | 1 | 1 | 2025-04-01 |
| Part VIIID | 55 | 55 | 2026-08-01 |
| Part IXa | 4,354 | 337 | 2026-06-01 |
| Part IXb / IXc / IXr / IXb+IXc | 51 / 158 / 19 / 1 | 0 | — |
| Part X | 8 | 8 | 2005-06-01 (stale) |
Extras §11 leaves open: the GTIN filename/hash reconciliation above; reading the v9.2.0 comms PDF; and a decision on
whether the XSD schemas and the comms PDF should be kept with the release — they are part of the delivery, tiny, and
the XSDs are the structural contract the loader is built against (the loader is schema-blind today, so nothing broke —
but nothing would have told us either).
Run on Matthew's machine against the same archive, with byte counts printed beside every hash so an empty read cannot pass
as a result. The seven XML members are byte-identical to what we hold — sizes 76,529,587 / 120,616,956 / 417,944 /
271,751 / 20,472,872 / 8,184,385 / 363,674 and SHA-256 153ae134…, 883ad186…, 092cc35c…, ada6981b…,
e3385fb3…, ff6b9758…, a7ebb7b8…, every one matching MANIFEST.txt. So the release we loaded is this download,
proven by hash rather than by size alone.
The nested GTIN zip holds four members — f_gtin2_0170926.xml (11,990,722 bytes: the same name and size we
store, so the odd …0170926 is TRUD's own naming, not a typo of ours; the filename reconciliation raised in the
handoff note is withdrawn), gtin_v2_0.xsd (1,453 bytes), docs/dmd_extract_comms_GTIN_9_2_0.pdf (189,241 bytes) and
docs/GTIN_Transfer_Tracking_Log.xlsx (95,253 bytes) — two further documents the import never saw, the second of them the
GTIN population's own transfer log.
The release note says nothing about reimbursement. docs/dmd_extract_comms_9_2_0.pdf extracts to 13,372 characters of
text, and a search of it for drug tariff, reimburse, deduction, activity fee and price returns nothing. dm+d's
own change note does not cover the tariff parameters either, so the §11 conclusion stands from a third direction.
Documents the loader never reads — now nine: the seven XSDs (35,461 bytes), the dm+d comms PDF, and inside the nested
zip gtin_v2_0.xsd, the GTIN comms PDF and the GTIN transfer tracking log.
The GTIN proof is closed — 2026-09-25 (later). The SHA-256 of the GTIN XML member itself
(unzip -p week392026-r2_3-GTIN.zip f_gtin2_0170926.xml | sha256sum) is
caaa16d894b6f989d0b1c33ff438b5f3d2b5d4aa19071e9afbd3122b65ff024f — identical to our stored copy. Every loaded
member of release 3170926 is therefore proven against Matthew's own download by hash: the seven core XML files and the
GTIN file. The delivery is fully accounted for.
Read 2026-09-25 from docs/dmd_extract_comms_9_2_0.pdf (287,907 bytes; 13,372 characters of text). The note is dated
21 September 2026, headed VERSION 9.2.0, and addressed to dm+d users. Five items, each with its position for us —
and not one of them mentions reimbursement, the tariff or the deduction scale, which is the third independent
confirmation of the §11 conclusion.
| Item | What it says | What it means for us |
|---|---|---|
| GTINs to be published in the SNOMED CT UK Drug Extension | NHS England will publish dm+d GTIN information inside the SNOMED CT UK Drug Extension so the data is available to users; only GTINs held within dm+d are included; planned go-live release 43.1.0 on 28 October 2026, date to be confirmed nearer the time | A second route to data we already hold (dm+d's own block, 101,309 GTIN rows). Our central SNOMED store — concepts 529,392 / descriptions 1,567,909 / relationships 3,548,588 / descendants 257,605, in the platform DB, loaded by hand from RF2 files — would carry it. A decision for the next SNOMED load, not code now; it feeds the parked barcode-lookup modification |
| SNOMED International description length limit | The maximum length of Fully Specified Name and Synonym descriptions rises from 255 to 4,096 characters; the affected descriptions first appear in SNOMED CT UK Edition Release 43.0.0, due 30 September 2026 (they enter via the August 2026 International Edition) | Storage is safe — snomed_descriptions.term is a text column (checked live). The risk is a 255-character assumption anywhere in code or forms around SNOMED descriptions, and the fact that our SNOMED load is manual: the store needs refreshing at 43.0.0 |
| Trade Family / Trade Family Group | NHSBSA maintains these concept classes in the dm+d supplementary extract files (since 6 July 2026; authored in line with the weekly release since 7 September 2026). Historical inactive Trade Family data created before July 2026 was not migrated into dm+d and remains only in SNOMED CT UK Drug Extension releases | New concept classes in an extract set we do not load — our archive holds the eight core files (§11). The loader is schema-blind, so nothing breaks; whether Trade Family is wanted is a scope question, and the pre-July history has no dm+d source at all |
| Specials/Imports added at DHSC request | Nine AMPs added to mitigate supply-chain shortages — desmopressin 150mcg/dose nasal spray (imported and special order), hydrogen peroxide 3% solution, Creon 10,000 and 25,000 capsules, Pancreaze, Pangrol 10,000 and 25,000 capsules, quetiapine 50mg modified-release tablets — with dm+d extract dates 2020–2025; DHSC requires them visible in prescribing and dispensing systems | They are ordinary AMP records inside the release, so they are in the store and already reachable by the client lookup. No action — but it explains why some unlicensed/imported products appear |
| Invalidations | None in this release. Advance notice of one concept to be invalidated mid-October as it was added erroneously — AMP Senokot 7.5mg tablets 12 Years Plus (38871711000001108) replaced by Senokot 7.5mg tablets (3386711000001106), with the AMPP pair 41170911000001103 → 3387111000001108 |
See §12.1 |
The note also carries two standing channels worth using: the monthly dm+d Supplier Workshop for IT system suppliers
(medicinestandards@nhs.net) — the natural place to raise the tariff/parameter gap and the TRUD API account — and the
first dm+d Annual User Survey (support.digitalservices@nhs.net, responses by Friday 2 October 2026).
The release carries an invalid flag per concept and we load it: invalid exists on VTM, VMP, VMPP, AMP, AMPP and
ingredient in the platform's reference_data models. In the live release (3170926), measured 2026-09-25:
| Rows | Invalid | |
|---|---|---|
| AMPP | 187,156 | 4,200 |
| AMP | 167,307 | 3,285 |
| VMPP | 37,397 | 1,161 |
| VMP | 24,662 | 620 |
| VTM | 3,250 | 106 |
The pharmacy-side product-information view never reads it. pharmacy_medicines/catalogue.py filters on the live
published release and nothing else — there is no invalid term anywhere in the module (search, pack detail, price and
GTIN lookups alike). So an invalidated pack can be searched, opened and shown with its price, its basis and its GTINs, with
nothing marking it as invalid — 4,200 packs today, and a named one after the mid-October invalidation.
This is not a defect against the spec (Part 2 v1 was specified as a capped name search over the live release) and it is
not for us to change unilaterally — hiding, flagging or leaving them is a display decision, because dispensing
systems legitimately still need to resolve historical codes. But it should be taken deliberately: today the reader is shown
a pack NHS has withdrawn as though it were current.
Checked live today at Matthew's steer (he holds an ODP account under his VeriPath address and expected the reimbursement
data to be fetchable from it). Two halves, one right and one wrong.
Right about the key: the ODP answers anonymously. It is CKAN, and
…/api/3/action/datastore_search?resource_id=…&limit=1 returned fields and records with no key (checked against the
DISPENSING_DATA_201404 resource). So the queries we would use need no credential — registration is useful for support and
rate-limit courtesy, not as an authentication gate.
Still not the tariff. Searching the whole portal: "drug tariff" → 162 datasets, tariff → 164; excluding FOI
responses leaves seven, and none is the Drug Tariff, a tariff price list, a deduction scale or a fee schedule:
All seven are prescribing, activity and spend datasets. The dispensing one, per contractor per month, carries FORMS,
ITEMS, TOTAL_PRESCRIPTIONS_PROFESSIONAL_FEES split into …_STND_DISC_RATE / …_ZERO_DISC_RATE, MUR/NMS declarations,
appliance-use reviews and stoma fees — counts of fees, not fee amounts, no deduction-scale percentage and no tariff
price. §5.7's conclusion therefore stands on a fresh check: the baseline tariff prices, the discount deduction scale and
the Single Activity Fee come from the monthly NHSBSA Drug Tariff documents, and from nowhere else.
What the portal is worth to us later — none of it today: PCA carries actual spend per drug, EPD carries what was
prescribed against SNOMED codes we already hold centrally, and the dispensing dataset carries the
zero-discount-rate / standard-rate split, which is the world the concession rule lives in. That makes the ODP a natural
accuracy cross-check for the reimbursement guardrail once it exists — not the source of its prices.
§11 and §13 proved what dm+d and the Open Data Portal do not carry; this section is the other half — the source that
does. Checked live today at Matthew's steer, so the next session starts from a map rather than re-deriving it. Nothing
here is a decision: D11 is still his, but it is now much cheaper to answer.
To be unambiguous, because it is the natural question: these part PDFs are not inside the dm+d delivery and never
have been — the zip carries the dictionary (the release held on VPS1 is exactly its eight XMLs, 228 MB, plus the
manifest), and the only tariff content in it is the 8,686 DTINFO price records inside f_vmpp2 — no deduction
rate, no fee, no scale anywhere (§11, audited member by member; re-checked on the stored release 2026-09-26). The Drug
Tariff is a separate NHSBSA publication: separate site, its own licence statement, and monthly where dm+d is
weekly. So the tariff is always a second source, and how it is acquired changes nothing about the TRUD pull or phase 2.
The hub page is
https://www.nhsbsa.nhs.uk/pharmacies-gp-practices-and-appliance-contractors/drug-tariff. It publishes the current
month as a set of per-part PDFs in a dated media folder — and the folder is the month of upload, not the month of the
tariff: the September 2026 edition sits under /sites/default/files/2026-08/. So discovery cannot be a constructed
path — read the hub page and take the links; the filename carries the month name.
| Document (all September 2026) | Bytes | Pages | Note |
|---|---|---|---|
Full Drug Tariff September 2026_0.pdf |
6,250,786 | 1,296 | every part in one document |
| Preface | 155,513 | 18 | |
| Part II — requirements; drugs for which discount is not deducted | 154,503 | 27 | |
| Part IIIA — Professional Fees (pharmacy contractors) | 74,595 | 2 | |
| Part IIIB — Scale of Fees (appliance contractors) | 67,213 | — | |
| Part IX — prices | — | — | also CSV and XLSX, every month back to at least Feb 2021 |
| VIIIA, VIIIB, VIIID, XVIIA, XVIIB(i) | — | — | current month on the hub page |
Two more pages matter for the routine: back copies is the monthly archive, and updates is a month-by-month
changelog (December 2025 → September 2026 currently) — free change detection, with no file links of its own.
pypdf (already installed on VPS1) extracts the 1,296-page edition to text with no OCR: Part V's table comes out as
from → to → rate triples and Part IIIA as labelled figures. Where the reimbursement parameters actually sit:
| Parameter | Location | September 2026 value |
|---|---|---|
| Single Activity Fee (England, per prescription) | Part IIIA, printed p.50 | 152p |
| Professional Fee (Wales — for contrast) | Part IIIA, printed p.50 | 105p |
| Deduction rate — Appliances | Part V, printed p.54, Table 1 | 9.85% (Part IX products) |
| Deduction rate — Generics | Part V, Table 1 | 20% (Category A and Category M of Part VIIIA) |
| Deduction rate — Brands | Part V, Table 1 | 5% (not an appliance, not Cat A/M) |
| Drugs for which discount is not deducted | Part II, printed p.1 | the group + individual item lists |
| Scale of Fees (appliance contractors) | Part IIIB | — |
| Consumables and containers | Part IV | — |
The deduction scale is no longer a graduated table. Since January 2024 it is three flat rates (Table 1 above).
The graduated table of £125 bands from £1 to £16,875 (5.63% → 9.9%) survives in Part V only as history and for the
October 2022 – December 2023 transition (Table 2, old/new weightings 85/15 → 15/85). Anyone building from §11's premise —
"the discount deduction scale" as a long table to parse — should know it is now three percentages, which move only when
DHSC decides.
The monthly parse a reimbursement guardrail needs is roughly 300 KB of PDFs (Part II + IIIA + IIIB), not the 6.25 MB
1,296-page edition — the full document is needed only as the archived reference copy, though it is the only place some
parts appear. The Part IX prices are already ours via dm+d (§11) and are separately published as CSV/XLSX.
The NHS Electronic Drug Tariff web app (https://www.drugtariff.nhsbsa.nhs.uk/, a JS publication app with hash routes
like #/00916580-DC/DC00916577/Home) serves its shell and its library JS, but its content endpoints are not at the
guessed paths — /Current/…, /api/…, /libs/Current/… all 404. The documents route is the reliable one today; the
app is worth a browser-based look only if a later decision needs per-page HTML.
Correction, 2026-09-26 (later the same day, from the specification work — a dated correction, not a silent edit).
The paragraph above understates the fetch. The deduction scale lives in Part V, and Part V is not published as a
separate part: the hub page's eleven files for September 2026 are the nine parts (II, IIIA, IIIB, IX, VIIIA, VIIIB,
VIIID, XVIIA, XVIIB(i)), the Preface and the full edition. So the 6.25 MB full edition is required, not merely
the archive copy, and v1's fetch is the full edition alone — it carries Part IIIA's Single Activity Fee (printed
p.50) and Part V's deduction rates (printed p.54). The "~300 KB" figure applies only to Parts II/IIIA/IIIB read on their
own (the discount-not-deducted list and the appliance scale), which v1 deliberately does not parse.
Spec: The Drug Tariff — Phase 4 Ingestion.
The operational view now has its own page. Cadence, order of events, human actions and open decisions per source are
written up as the Data Refresh Schedule — this section is
the evidence behind it, and §5.8 is the per-source requirements list it builds on.
============================================================