Status: AUTHORISED 2026-09-27, v1 of phase 6 — to be built. Matthew: "let's go with your proposed
plan … we can start with some basic and work our way from there." This is the first visible surface of the
pricing programme: the three NHS sources shown together against a pack, on the test client, so the shape can be
seen and argued with before the substance (the pack matching) is built.D-A — RESOLVED 2026-09-27: the concession figures reach a contractor, on a licence we already hold. Matthew ruled
admin-only out — "this approach will not achieve the purpose" — and the answer is not a consent waiver: the same
216 lines are published by NHSBSA/DHSC on the Drug Tariff updates page (OGL v3,consent_required = false),
and they agree with our CPE-derived store exactly (0 missing, 0 extra, 0 prices differing — phase 5 spec §19). So
the contractor-facing figures come from that source, each citing its dated notice; the CPE rows stay behind
their closed gate, admin-only, as the cross-check (§4 W4, §4 W6).Parents: Central Reference Data (phase 6 —
"see the data", and phases 7–8 it leads to), Data Feeds — Pharmacy View
(the page this extends), Price Concessions — Phase 5
(§18 item 3, the client-facing surface). Assessment behind this spec: the dated review on
Processing Imported Data.
For a pack, show the three sources together with their processing position; and add one read-only page listing
the month's concession lines exactly as they arrived. That is all.
Deliberately not in v1: any matching algorithm, the manual override table, the supplier-price join, the margin or
best-supplier column, exports, a cached subset per tenant, any write to the store, and any hard-coded audience for
the concession elements — their visibility is read from the licence register, never set in code (§5, D-A). v1
exists so the display can be judged before the arithmetic is relied on.
Read live from the platform database, not inferred. The figures below are what the view will be built against.
| Fact | Value |
|---|---|
| Releases | dm+d 3170926 published, live (2026-09-21) · Drug Tariff 2026-09 published, live (2026-09-01) · Price concessions 2026-09 imported, is_live false — the gate is shut |
| Tariff parameters on the live tariff release | deduction_rate_generics 20% (p.54) · deduction_rate_brands 5% (p.54) · deduction_rate_appliances 9.85% (p.54) · single_activity_fee 152 pence (p.50) |
| dm+d price inventory | 187,156 AMPP rows · 92,294 with a price · 94,862 without · none stored as zero |
| The pack's Drug Tariff category is in dm+d | DmdDrugTariff — 8,686 rows over 13 pay_category_codes, and DmdLookup table DT_PAYMENT_CATEGORY names them all: Part VIIIA Category A / C / H / M, Part IXa / IXb / IXc / IXr, Part VIIIB, Part VIIIC, Part VIIID, Part X, Parts IXb and IXc |
| dm+d also holds the tariff price itself | DmdDrugTariff.price_pence — 4,439 of the 8,686 rows |
| Absence has named flavours | PRICE_BASIS: NHS Indicative Price · No Price Available · No Price product centrally funded · No Price priced when manufactured |
| The month's concessions | 398 stored rows over 206 distinct drugs; the month's effective set is 216 (drug, pack size) lines |
| The same values, licensed (found 2026-09-27) | NHSBSA's Drug Tariff updates page publishes them month by month as dated "Drug Tariff News — Part VIIIA reimbursement prices (issued DD Month)" notices: 7 notices for September 2026 (04, 16, 17, 18, 22, 24, 25) carrying 216 rows / 216 distinct (drug, pack) — against our store: 0 missing, 0 extra, 0 prices differing. Our register already records that source as OGL v3, consent_required = false, and the tariff ingestion already fetches the same publisher monthly. This is the client-facing source of record; CPE is the alert and the cross-check (phase 5 spec §19) |
| The reimbursement level | the VMPP — dm+d's own Drug Tariff block is keyed vmpp_id, and every AMPP under a VMPP shares it |
| The pharmacy app's route to it | read-through on default (16 reference_data tables), already used by pharmacy_medicines, which carries managed=False mirrors for ReferenceSource, ReferenceRelease, DmdAmp, DmdAmpp, DmdAmppPrice, DmdGtin |
| What is not linked | StockItem has no dm+d reference of any kind (item_code, pip_code, barcode, pack_size, form, strength — all free text apart from the codes); SnomedItemMapping links a prescription's SNOMED code to a StockItem |
Worked example — why the levels matter. Aciclovir 400mg tablets, pack 56:
41485611000001107 — Aciclovir 400mg tablets (the0011 — Part VIIIA Category M, so the rate that attaches to it is 20%;It also shows why the match is not name-equality: 8 AMPPs share that VMPP-shaped pack (Accord, Almus, Crescent,
Sigma, Kent, Waymade, DE, Avianta) with prices from 255p to 600p, and the concession line names none of them.
contractor's stock item → AMPP → VMPP
(PIP / barcode, no dm+d link) (the brand they hold) (the pack that is reimbursed)
A contractor buys an AMPP, the NHS reimburses the VMPP for Cat A/M and Part IX, and our supplier-price imports
land on a StockItem which today has no bridge to either. v1 does not build that bridge; it must not pretend the
bridge exists. Naming the three levels now is what keeps the matching phase (and phase 7's accuracy argument) honest.
W1 — the mirrors. Add narrow managed=False mirrors in pharmacy_medicines for the tables this view reads:
ReferenceTariffParameter, DmdDrugTariff, DmdLookup, DmdVmpp, DmdVmp, and the two concession tables
(ReferenceConcessionUpdate, ReferenceConcessionPrice). Same pattern as the existing mirrors: managed = False,
db_table set, no table created, no store schema touched, no .save() on any of them. Any index the queries
need is added by the platform app's migrations, never from the pharmacy side.
W2 — the pricing panel, on the existing pack detail page (/medicines/). Read-only, no new permission, showing:
DmdGtin carries one, and the Drug Tariff category inPart VIIIA Category M, resolved through DT_PAYMENT_CATEGORY), or "no Drug Tariff category".£0.00, never blank.W3 — the month's concession list, one read-only page — suggested route /medicines/price-concessions/. The
month's lines as NHSBSA published them: drug text, pack size, price, the notice and its issue date, grouped by
notice in issue order, with the NHSBSA/DHSC attribution and the release identity. Searchable by name, capped, no
export. Read beside the pack panel, this is what lets Matthew see what is being read correctly and what is not.
(The CPE-derived view of the same month already exists in the phase-5 admin screens, so nothing is lost by keeping the
CPE rows off the contractor surface.)
W4 — the two sources and the audience (the consent boundary), rewritten 2026-09-27 after §19 was found. The purpose
is a contractor comparing their own purchase price with the NHS position, so the concession figures must reach a
contractor's screen. They can, on a licence we already hold: the values are published by NHSBSA/DHSC on the
Drug Tariff updates page — the register records that source as OGL v3, consent_required = false — and the 216
lines agree with our CPE-derived store exactly (phase 5 spec §19). So:
may_publish false, release imported and not live, and it staysW5 — the verifier. veripath-ops/scripts/verify-nhs-pricing-view.py, re-runnable, run inside the container against
the test client host (the phase-4/5 pattern), asserting every number below rather than eyeballing a screen.
W6 — the client-facing concession source (added 2026-09-27). A new reference source for the NHSBSA Drug Tariff
News Part VIIIA reimbursement price notices, on the same publisher and the same discovery rule as the tariff: read
the Drug Tariff updates page and take the notices that are there — never construct a URL, take the name, pack size
and price from each dated notice, one release per month with each notice recorded as an update (the 5th/6th-update
pattern of phase 5, reused), licence OGL v3, no consent gate, attribution NHSBSA / DHSC. The page is HTML
lists (name pack £price), so the parse is simpler than the tariff's — no PDF, no OCR, no size heuristics. The
CPE stream's cross-check against it is the acceptance test: the month's lines must agree 0 missing, 0 extra, 0
prices differing (proved by hand on 2026-09-27; it must be a check that runs).
W6 — the cadence, which is the part that had to be answered (phase 5 spec §19.1). The bulletins run through the
month, not once at the end: NHSBSA issued 7 notices in September 2026 (04, 16, 17, 18, 22, 24, 25) and 7–9 a
month across the last six months — the same rhythm as the CPE stream, and the same amendment behaviour (the
Chlordiazepoxide line moved £85.45 → £92.86 between the 02 and 25 September notices). So the routine polls the updates
page daily (one small HTML GET, no credentials), and a new notice is a new update on the month's release:
Range and Last-Modified all fail on this page (tested: 200Last-Modified regenerates daily either way), so do not build a clever cache check.ReferenceFetchAttempt log.| # | Decision | Status |
|---|---|---|
| D-A | The consent boundary — resolved by changing the source, not the licence. The contractor-facing concession figures come from the NHSBSA Drug Tariff News notices (§4 W6): OGL v3, no consent gate, each figure citing its dated notice, attributed NHSBSA / DHSC. The CPE-derived rows remain behind their closed gate and are shown to platform admin only, as the cross-check that proves the NHSBSA parse (the two agree 0/0/0 — phase 5 spec §19). No consent is sought from CPE and none is needed, because the values a contractor sees no longer come from CPE. Visibility on both surfaces is read from the source row, never hard-coded | RESOLVED 2026-09-27 — Matthew's ruling that admin-only fails the purpose, answered by the licensed route. Caveat for him to confirm: this makes NHSBSA the source of record for client-facing concession prices, with CPE as the alert — if he prefers to seek CPE consent anyway, the CPE gate opens by the register as before and the display follows it |
| D-B | Which base price the deduction attaches to — dm+d's VMPP tariff price, or the AMPP published price. v1 shows both, labelled, and names the applicable one from the category, rather than the app silently choosing | Deferred to v2 / phase 7's accuracy argument — v1 is built to make the choice judgeable, not to make it |
| D-C | Route and page naming (/medicines/price-concessions/) |
Suggestion only; a one-line change |
test-client-pharmacy, a searched pack shows §4 W2 items 1–3: the category in words or "no Drug Tariff£0.00 anywhere, and every absence carries its reason.pharmacy-price-counts.py before and after), and no new write path exists to any store table.dsp_clinic.test_settings; verifier re-runnable and committed; the live check run on the testThe three sources are live, so nothing is blocked externally; and the pieces that the display needs — the pack's
category, the tariff price, the concession lines — are already in the store, and since 2026-09-27 the last of them
is also available on a licence that permits client publication (§4 W6). What is missing is only the pack link
— which, measured on 2026-09-27, is a normalisation rule (98.8% of the whole published history, every line of the
last 16 months) rather than the open-ended work it was assumed to be (the Pack Link spec §2).
So v1 shows the sources with their processing
position and no link; the next refinement is the link (name + pack size → VMPP, with a review queue and an override
table) — now specced and evidenced separately: Data Feeds — the Pack Link
— then the derived figure, then the contractor's own purchase price beside it (phases 7–8).
Update, 2026-09-27 (later) — the three deploy defects are FIXED and re-verified, and all four notes are closed.
The defect list below is kept as the dated history of what was found, not as the current state. OpenCode redeployed
by revision —feature/nhs-pricing-view@e0d4f9aandfeature/pharmacy-price-concessions-view@
3f45af7, both trees clean, with the bundles now beside the deployment at/root/work/bundles/— and I
reproduced every count myself on the app host:reference_data164 OK (1 skipped),pharmacy_medicines
31 OK,tenancy pharmacy pharmacy_dispensing pharmacy_medicines174 OK, and the tightened verifier
(veripath-ops@9396fc7) 47 PASS / 0 FAIL. The four notes are closed with evidence:
the SAF check now asserts the per-pack net is the tariff figure less the rate (208p) and is not
basis + fee(412p); the row count iseffective == 216withrows >= effective; the ordinal-gap alert is
suppressed for a signal-only stream (is_signal_only, commit8d5576d) and the only live alert is the unrelated
dm+d key notice; andconcessions_release()consults the source row'smay_publish, with
test_a_gated_notices_source_hides_the_concessionscovering it. Residual, and not a defect of this build: eight
untracked stray files sit at the root of the app host'sgp_booking_appcopy (agent_config_list.html,
base_dashboard.html,home.html,urls.py, …) — they predate this build and are the only thing keeping
git statusnon-empty there.
Reproduced independently, not taken on report. I piped the committed verifier
(veripath-ops/scripts/verify-nhs-pricing-view.py) into pharmacy_app on the app host myself — 47 PASS, 0 FAIL —
and then read the store directly, twice over, rather than trusting the app's own output:
| Check | Result (2026-09-27) |
|---|---|
The drug_tariff_news source |
routine nhsbsa_notices, cadence monthly, consent_required false, licence OGL v3, attribution NHSBSA / DHSC, may_publish true |
Release 2026-09 |
published and live; 7 notices applied in issue order (04, 16, 17, 18, 22, 24, 25 September — 7 + 67 + 28 + 15 + 46 + 17 + 36 rows) |
| The month | 216 rows / 216 distinct lines, 7 rolled-over, 0 amended, every price positive |
| The source change | against the CPE-derived store: 0 missing, 0 extra, 0 prices differing |
| The worked example | Aciclovir 400 mg tablets 56 — category 0011 Part VIIIA Category M, dm+d tariff 260p, rate 20% (release 2026-09, p.54), net 208p, concession 361p with a dated notice |
| Cost | HTTP 200, 574,887 bytes, 0.21–0.25 s — inside the ceiling, and a same-day re-run is a no-op |
| The signal | 2 rows for the real 5th/6th-update alerts, with their real Message-IDs and received dates (2026-09-24 / 2026-09-25), both matched to a notice |
| The gate | price_concessions still consent_required true, consent_granted false, no live release |
| The client-facing path | NOTICES_SOURCE_CODE = 'drug_tariff_news', and no reference to price_concessions anywhere in the pharmacy view's code or templates — the CPE rows cannot reach a pharmacy screen |
| Health | the notices source reads state current, not overdue; no lagging signals |
The product logic is sound and the data is right. Three defects sit in the deployment, and one note applies to the next phase.
gp_booking_app is onfeature/price-concessions-ingestion at 8abc4bd, with the whole phase-6 build as uncommitted working-tree0009/0010 migrations and tariff_news*.py untracked). The claimedfeature/nhs-pricing-view and its commits are not on the host at all, and the bundles live on VPS1,git checkout/git stash would silently revert the build,git clean -fd would delete untracked migrations whose state the database has already applied. The pharmacy27dc604 + dirty tree).tariff_news_fixtures.py,test_concessions_signal.py, test_tariff_news_parse.py and test_tariff_news_task.py are absent, andtest_health.py / test_lifecycle.py are the old versions. The suite on the host therefore reportsProgrammingError: relation "reference_data_dmdvmpp" does not exist). The cause is the same: the host still has the old tests.py (18 tests,MIRROR_MODELS list without the new mirrors) while the view now reads them. The bundle's tests.py — 30 tests,test_cpe_rows_never_render_for_a_pharmacy_user — passes; so the fix is the deploy,Notes that do not block, but should be closed with the next handover:
net != net + 152 is true always) — the substance is §5's 208p, and thetest_the_saf_is_never_folded_into_the_pack_figure. Worth fixing so the check means something.216 is used for both stored rows and effective lines: the first month with an intra-montheffective == expected androws >= effective.ordinal_gapslagging_signals).concessions_source() does not consult may_publish — it returns the notices source unconditionally. That worksVeriPath internal. Dummy/synthetic data only — no patient-identifiable information appears in this work, and no
credential or internal address is recorded here.