WHO Inventory & Nomenclature Gap Checklist
Derived from comparing Ovahol against Section 3 (Inventory management) and Section 4 (Medical device nomenclature and classification) of Inventory and maintenance management information system for medical devices (WHO, 2025) — publication page.
Items are grouped into Priority 1 (UDI — highest patient-safety impact, WHO's recommended backbone for recalls), Priority 2 (GMDN nomenclature — high value for cross-facility normalization), Priority 3 (Inventory financial & operational rollups), and Priority 4 (Nice-to-haves & metadata extensions).
Priority 1 — UDI (Unique Device Identification)
WHO treats UDI as the backbone of fast, precise recall handling and of grouping identical models for consistent maintenance. Previously Ovahol had no udi, device_identifier, production_identifier, lot_number, batch_number, or expiration_date field anywhere in the devices module — SerialNumber and DeviceTagNumber existed, but neither was the FDA/EU-regulated UDI construct WHO describes; they are facility-local identifiers, not a manufacturer-assigned globally unique code. Today that baseline is closed: the model catalog carries the fixed udi_device_identifier (UDI-DI, labeler + model) and device_record carries the variable UDI-PI fields lot_number and expiration_date, while the facility-local SerialNumber / DeviceTagNumber identifiers remain distinct from these regulated UDI values.
- [x] Decide where UDI fields belong: on
device_record(per physical unit) vsmodel(per catalog entry). UDI has two parts — a fixed device identifier (labeler + model/version, effectively per-model) and a variable production identifier (lot/batch, serial, expiration date, effectively per-unit) — so this likely means a UDI code field onmodelplus lot/batch/expiration fields ondevice_record, mirroring how UDI itself is structured. - [x] Add
udi_device_identifierto the model catalog (internal/modules/devices/dto/model.go,internal/modules/devices/service/model.go), following the same pattern as the existingEMDNCode/EMDNTermfields on classification. - [x] Add
lot_numberandexpiration_datetodevice_record(internal/modules/devices/dto/device_record.go/facility_device_record.goservice), alongside the existingSerialNumber/ManufactureDatefields. - [x] Migration for both (core schema for model, tenant schema for device record — same split as the rest of the catalog vs. record data).
- [x] Wire UDI into recall matching: today
RecallNoticeis scoped to a device record manually; consider whether recalls should be matchable by UDI device identifier across all units of the same model, which is the actual workflow WHO describes (a recall hits a model/lot, not one unit). - [x] Export: add the new fields to
ExportedDeviceRecord(internal/modules/devices/facility_device_records.go) and its Excel column / JSON field, same as every other field added to the export this session. - [x] Frontend: add UDI/lot/expiration fields to the device create/edit forms and detail view.
Priority 2 — GMDN nomenclature code — done
Ovahol already has EMDN support end to end — EmdnCode/EmdnTerm on core_public.device (the checklist originally said "device_classification table," which doesn't exist by that name — the actual table is device; corrected during implementation), surfaced on the device record response, plus a full DictionaryRecommendationService workflow for proposing and reviewing classification entries (internal/modules/devices/service/dictionary_recommendation.go). WHO specifically highlights both GMDN (Global Medical Device Nomenclature) and EMDN (European Medical Device Nomenclature) as the two primary systems countries adopt.
- [x] Added
gmdn_code/gmdn_termcolumns tocore_public.device(backend/repository/migrations/060_device_gmdn_code.up.sql), alongside the existingemdn_code/emdn_term— including rebuilding the table's full-text search vector to index GMDN too. - [x] Surfaced GMDN in
DeviceResponse(the classification DTO) and on the device record response (internal/modules/devices/dto/device.go,dto/device_record.go), mirroringEmdnCode/EmdnTermfield-for-field. - [x] Added GMDN columns to the Excel device export — and, since EMDN wasn't in the export either (a separate pre-existing gap), added both EMDN and GMDN together rather than exporting GMDN alone.
- [x] Extended
DictionaryRecommendationService's device payload (deviceRecommendationPayload) so proposals can carry GMDN fields alongside EMDN. - [x] Frontend: GMDN code/term on the device classification catalog (list, columns, CSV export, create/edit dialog), the recommendation review form, and every device-record detail view that already shows EMDN (facility-level, org-level, and the printable report preview).
- Found and fixed along the way: while extending the shared
ListDeviceRecords/GetDeviceRecordSQL templates (used directly by the facility-scoped repository), discovered thatinternal/modules/devices/repository/org_device_record.goreuses those same raw SQL templates for the org-level cross-facility listing/detail endpoints viaLoadRaw, but its own hand-duplicatedScan()calls had silently drifted out of sync with every column added since the UDI work (lot/expiration, installation fields, installer/vendor fields, and now GMDN) — a latent bug that would have broken those endpoints at runtime. Fixed both scan sites to match the verified-correct field order.
Priority 3 — Inventory financial & operational rollups — done
WHO Section 3 emphasizes facility-level asset valuation rollups (total acquisition value, depreciated replacement value, distribution by age cohort, distribution by risk category). This checklist item originally claimed the raw fields already existed on device_asset (cost/purchase_date/warranty_expiry_date) and a device_risk_level column — both false on inspection: device_asset carries none of those fields (the real financial data lived only in the separate procurement module's acquisition_record, which isn't a per-unit snapshot suitable for this rollup), and no device_risk_level column exists anywhere. Corrected the premise and built the fields directly rather than working around the gap, per explicit user direction to add them on device_record.
- [x] Added
warranty_expiry_date(DATE) andacquisition_cost(NUMERIC(12,2)) todevice_record(internal/database/tenant_migrations/versions/048_device_record_inventory_financials.sql), wired through DTO/mapper/service/repository (all 3 SQL queries + 5 Go scan sites, including the org-levelLoadRawreuse inorg_device_record.go) and the create/edit device-record forms + detail views (facility- and org-level). - [x] Added
expected_lifespan_years(SMALLINT) tocore_public.model(repository/migrations/061_model_expected_lifespan.up.sql) — no lifespan field existed anywhere in the schema, so this was a genuine gap, not just a rollup convenience; wired through the model create/edit dialog alongside the other catalog specs. - [x] Added a
GetInventorySummaryendpoint to the devices module (GET /tenant/device-records/stats/inventory-summary,repository/sql/device_record.sql) returning:- Total active device count and total estimated value (sum of
acquisition_cost). - Count and value grouped by risk level. Sourced from the model's FDA regulatory classification (
lookup.regulatory_class_fda.risk_level, values LOW / MODERATE / HIGH) as the closest existing proxy — no standalone device risk-level field exists. - Count and value grouped by age cohort (<5 yrs, 5–10 yrs, >10 yrs) based on
installation_date. - Count of devices past their expected lifespan (
installation_date + model.expected_lifespan_years < today), flagged for decommissioning review. - Warranty coverage breakdown (under warranty vs. expired vs. unknown), based on
warranty_expiry_date.
- Total active device count and total estimated value (sum of
- [x] Exposed via a new
InventorySummaryCardon the tenant facility dashboard (features/dashboard/components/InventorySummaryCard.vue), alongside the existing maintenance/retirement stat cards.
Priority 4 — Metadata extensions & nice-to-haves
- [x] Power requirement specification: WHO Section 3.2 lists power type (mains / battery / dual), voltage, frequency, and backup power requirement as core inventory attributes. Audit found this mostly already covered by pre-existing schema this checklist item didn't account for:
model.rated_voltage/rated_frequency(voltage, frequency) andmodel.power_source_id(Main Grid/Generator/Solar/Battery/Hybrid lookup) plusdevice_record.backup_power_source_id(backup power requirement, as an actual selected source rather than just a yes/no flag) — all predate this session. The one genuine gap: no way to flag a device as running on both mains and battery with automatic switchover (common for patient monitors/infusion pumps). Addeddual_power_capable(boolean) tocore_public.model(repository/migrations/062_model_dual_power_capable.up.sql), wired through DTO/mapper/service/repository and a checkbox onManageModelDialog.vue. - [x] Software / firmware version tracking: WHO Section 3.4 recommends tracking current software/firmware version separately from hardware revision for medical devices with embedded software, especially for cybersecurity advisory response. Ovahol already had
firmware_versionon the model catalog (a default/baseline); added an optional per-unitfirmware_version_overrideondevice_record(internal/database/tenant_migrations/versions/047_device_record_firmware_override.sql) so units that drift onto different firmware after staggered updates can be tracked individually — the effective firmware version (override-or-model-default) is whatDeviceRecordResponse.firmware_versionreturns, with the raw override also exposed separately. Wired through DTO/mapper/service/repository and the device-record create/edit forms + detail views.