Status: BUILT 2026-09-12 — verified by Hermes 2026-09-12. Phase 1 of the agreed plan in
module-map. Phase 0 is complete (repository,
pharmacy sector + first tenant, Keycloak realm, app-engine stack, HTTPS). See As delivered
below for what was checked and the four places the spec met reality differently.
A pharmacy cannot lawfully open to the public without a designated Responsible Pharmacist on duty
and a record of who that was, when, and under which registration. Everything else — dispensing, stock,
the counter — presupposes an identified, authorised person on site. So Phase 1 establishes identity
and the legal log, before any operational feature is built.
In: identity/role wiring for pharmacy users; the pharmacy premises profile; capture of the GPhC
premises number and ODS code with a verification record; and the Responsible Pharmacist Log.
Explicitly out (later phases — do not build ahead):
patient or clinical models, dispensing, EPS, stock/batch, counter POS, payments, patient
notifications, and the automation that provisions a new client pharmacy end to end.
The rule is unchanged (see module-map): we hold client-organisation data only; no patient data at any
tier, ever.
| Data | Where it lives |
|---|---|
| Organisation identifiers — ODS code, GPhC premises number, tenant name/subdomain | Central tier, on the existing tenancy.Tenant record. Do not duplicate them into a new app model. |
| Premises profile detail — trading name, address, telephone, contact email | Client tier (sector_pharmacy). Business contact detail is not held centrally. |
| Pharmacist personal data — name, personal GPhC registration number, shift history | Client tier, on the RP log entry itself (name snapshot + personal GPhC number), so the legal record is self-contained. See finding 1 in As delivered: the platform's authentication user records stay central, as they do for every sector. |
| Audit of who did what, when | Client tier, with no clinical or patient content |
infrastructure.ClientVPS.Sector does not need a pharmacy choice for this phase — that model
serves client-VPS provisioning, which Phase 1 does not touch. Leave the migration alone for now.
W1 — Identity: pharmacy realm login and role mapping.
The app's OIDC endpoints are currently hardcoded to the veripath realm. Wire the tenant-aware OIDC
so pharmacy users authenticate against realm pharmacy (clients pharmacy-booking-app / erpnext)
and map its four client roles onto the app's role model and existing RBAC middleware.
Acceptance: a user with RESPONSIBLE_PHARMACIST authenticates; a user with only COUNTER_ASSISTANT
cannot reach any prescription or clinical surface; gating is enforced at the view/queryset layer,
not merely hidden in navigation — the dental app's
role permissions matrix records exactly
the gap that leaves when nav gating is treated as authoritative.
W2 — Premises profile.
The pharmacy's own operational record (trading name, address, telephone, contact email) on the client
tier, editable by the Business Manager role. Central keeps only the identifiers already on Tenant.
W3 — GPhC premises number and ODS code capture + verification record.
Capture both, with a verification record: who verified, when, by what method, and a reference to the
evidence. This is a compliance artefact for DSPT, not just a field — the record of how we know the
premises is registered matters as much as the number. See decision D2.
W4 — Responsible Pharmacist Log (the core deliverable).
clean() check alone, which cannot survive concurrent requests. The dental app'sPROTECT and nothing may cascade.RESPONSIBLE_PHARMACIST.W5 — Storage placement.
Confirm the migrations route W2–W4 to the client tier and that nothing lands centrally except the
identifiers already on Tenant. Verify against the router's PHARMACY_APPS partition.
W6 — Audit and DSPT evidence.
Each state change records who, when and what — with no clinical or patient content anywhere in the
trail.
W7 — Synthetic demo seed.
A seed command creating a synthetic premises and at least two synthetic pharmacists — clearly synthetic
British names, GPhC-style numbers marked as test values, no real registrant details — so the RP log can
be demonstrated and documented. This is what Hermes needs to write the user guides and run the live
walkthrough, so it is a deliverable, not a nice-to-have.
W8 — Report back.
File pointers for the doc pass, plus any place where this spec proved wrong in practice (the Phase 0
findings were all folded back into the module map — keep doing that).
D1 — Cryptographic signing of RP entries. The research proposed signing each entry through the AES
portal. Recommendation: defer. Authenticated sign-on plus an immutable, append-only audit trail
satisfies the immediate legal record; the AES portal signs documents (PAdES PDFs) and a shift event
is not a document, so the fit is unproven. Revisit as a DSPT non-repudiation item once the log exists.
D2 — Source of GPhC/ODS verification. The research asserts an automated GPhC registry API.
Recommendation: do not build against an assumed API. Capture the numbers and record manual
verification evidence; investigate an automated check only if a genuine registry API is confirmed to
exist, and treat that as its own small piece of work.
Phase 1 is done when: a pharmacist can sign on and off; the one-active-RP rule holds under concurrent
requests; role gating is verified at the view layer; the premises profile and verification record
exist; nothing patient-related is stored centrally; the test suite is green; and the synthetic seed
demonstrates the flow end to end.
Built on branch phase1/onboarding-rp-log (= dev), deployed to the app-engine stack. Hermes
verified the following against the artifacts, not from the build report:
| Verified | Evidence |
|---|---|
| Pharmacy realm login is live | A request to /oidc/authenticate/ on the live site redirects to the pharmacy realm with client pharmacy-booking-app — checked directly against the running site |
| Roles and gating | Four pharmacy roles on the user model; a base PharmacyRoleRequiredMixin with staff / responsible-pharmacist / business-manager variants; middleware gating /pharmacy/; every view carries the appropriate mixin, so gating is at the view layer, not the nav |
| Client-tier storage | The four tables exist in the client Postgres database (pharmacy_pharmacypremises, pharmacy_premisesverification, pharmacy_responsiblepharmacistlog, pharmacy_pharmacyauditevent) — inspected directly in the live database |
| One active RP per premises | A live partial unique index: CREATE UNIQUE INDEX one_active_rp_per_premises ... (premises_id) WHERE is_on_duty — enforced by the database, not by a validation check. Proven to bite: a synthetic second on-duty entry for the same premises is rejected with a duplicate-key error, while an off-duty entry coexists (both probes rolled back; demo data untouched) |
| Append-only | The model's delete() raises rather than deleting; the record carries the name snapshot and personal GPhC number |
| Synthetic demo data | A synthetic premises exists in the client database; two demo pharmacist accounts exist in the pharmacy realm, enabled and carrying the role attribute |
| Test suite | Run by Hermes on 2026-09-12 using the documented runner: 20 tests, all passing (the runner builds the test schema from the models, because the shared migration history cannot build a fresh database) |
| Demo credential | Rotated out of the wiki on 2026-09-12; the live value is held host-local, and the seed now requires it from the environment. Verified: logging in with the rotated credential succeeds |
| Navigation | Premises and Verification added to the navigation for the Business Manager and Responsible Pharmacist roles (they were dashboard-button-only) |
| Ops hygiene | The ops working copy flagged on 2026-09-12 is now committed and the tree is clean |
Exercised live, 2026-09-12. The RP cycle was then run end to end against the live site — a real
login through the pharmacy realm, sign-on, dashboard, history page, sign-off — and the outcome
confirmed in the client database: one log entry with both a sign-on and a sign-off timestamp and the
on-duty flag cleared, plus two audit events (RP_SIGN_ON, RP_SIGN_OFF). The dashboard returned to
"No Responsible Pharmacist is currently on duty." and the history row showed Off. So the workflow,
the UI and the audit trail are all verified, not merely wired.
A separate synthetic probe confirmed the one-active-RP constraint actually bites at the database level:
the first on-duty entry was accepted, a second concurrent on-duty entry for the same premises was
rejected with a duplicate-key error, and an off-duty entry was accepted alongside it. Both probes were
rolled back, so no test residue remains in the log.
Authentication users are central, not client-tier. The boundary table above originally put
pharmacist names on the client tier. The platform's users app is central for every sector
(dental included), so pharmacy auth users stay central to avoid a cross-database auth refactor; the
RP log entry carries the name snapshot and personal GPhC number, so the legally mandated record is
client-tier and self-contained. Recorded as a deviation — Matthew may want auth identities moved
too, which is a separate piece of work.
The pharmacy realm is not a partner org. The apex tenant has no partner_org, so the OIDC
backend needed an explicit pharmacy branch rather than the partner path. A future pharmacy client
will also need its client id set to pharmacy-booking-app.
A fresh test database still cannot be built from migrations. The GP lineage carries duplicate
and orphaned migration records, so a test settings module was added that builds the schema from the
models instead. This is the migration-history cleanup already flagged in the handoff — it remains
its own piece of work, and it is a platform issue, not a pharmacy one.
The user role field needed widening for the longer pharmacy role names — a migration on the
shared central database. Because that app is central, the GP repository does not carry it: if GP
ever adds pharmacy roles, the two histories must be reconciled.
Sign-off is not restricted to the pharmacist on the shift — DECIDED 2026-09-14 (Matthew): the entry
will record who closed it. The behaviour stands (closing a colleague's shift is permitted — handover,
or a forgotten sign-off), but the record must say so itself rather than relying on the audit trail. The
RP log entry therefore gains a "signed off by" snapshot (user reference plus name, the same pattern
as the rest of the log) written by whoever performs the closure, and a migration to go with it. It is a
small addition to the log, not a redesign, and it is now authorised — a Phase 1 follow-up for OpenCode
rather than a Phase 2+ item.
Premises and verification screens admit the Responsible Pharmacist as well as the Business
Manager. The spec's W2 said the premises profile would be editable by the Business Manager; as
built, the gate deliberately includes the RP ("who owns the premises"). Verified live: a
pharmacist-only demo user receives the premises and verification screens normally. Defensible, but
it is a wider permission than the spec stated, so it is recorded rather than glossed.
Premises/verification navigation — resolved 2026-09-12. These screens were reachable only from
dashboard buttons; nav entries were subsequently added for the Business Manager and Responsible
Pharmacist roles.
CORRECTED 2026-09-12 — an earlier finding here was wrong. This page previously recorded that the
demo pharmacists were not in the RESPONSIBLE_PHARMACIST Keycloak group, because the admin user
representation shows an empty groups field. Querying the group's members directly shows
demo.rp.one and demo.rp.two are members — that representation simply omits group membership. The
client carries no groups protocol mapper, so the app reads the role from the role user attribute;
that is the documented contract, not a defect. Left visible as a correction so the mistake is not
repeated: on this platform, group membership cannot be judged from the user object.
Both are in the shape recommended here, but they were never explicitly answered by Matthew — worth
confirming, particularly D1, since it bears on the DSPT non-repudiation story.