Skip to content

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) vs model (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 on model plus lot/batch/expiration fields on device_record, mirroring how UDI itself is structured.
  • [x] Add udi_device_identifier to the model catalog (internal/modules/devices/dto/model.go, internal/modules/devices/service/model.go), following the same pattern as the existing EMDNCode/EMDNTerm fields on classification.
  • [x] Add lot_number and expiration_date to device_record (internal/modules/devices/dto/device_record.go / facility_device_record.go service), alongside the existing SerialNumber/ManufactureDate fields.
  • [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 RecallNotice is 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_term columns to core_public.device (backend/repository/migrations/060_device_gmdn_code.up.sql), alongside the existing emdn_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), mirroring EmdnCode/EmdnTerm field-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/GetDeviceRecord SQL templates (used directly by the facility-scoped repository), discovered that internal/modules/devices/repository/org_device_record.go reuses those same raw SQL templates for the org-level cross-facility listing/detail endpoints via LoadRaw, but its own hand-duplicated Scan() 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) and acquisition_cost (NUMERIC(12,2)) to device_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-level LoadRaw reuse in org_device_record.go) and the create/edit device-record forms + detail views (facility- and org-level).
  • [x] Added expected_lifespan_years (SMALLINT) to core_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 GetInventorySummary endpoint 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.
  • [x] Exposed via a new InventorySummaryCard on 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) and model.power_source_id (Main Grid/Generator/Solar/Battery/Hybrid lookup) plus device_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). Added dual_power_capable (boolean) to core_public.model (repository/migrations/062_model_dual_power_capable.up.sql), wired through DTO/mapper/service/repository and a checkbox on ManageModelDialog.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_version on the model catalog (a default/baseline); added an optional per-unit firmware_version_override on device_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 what DeviceRecordResponse.firmware_version returns, with the raw override also exposed separately. Wired through DTO/mapper/service/repository and the device-record create/edit forms + detail views.

Ovahol developer documentation