Kulcare Docs
DeveloperCare plansBackend

Backend Endpoints for Longitudinal Health Records

Overview

This document defines the backend API endpoints required to support the existing frontend LHR UI at /cms/careplan/patient/:patient_id.

Primary LHR Endpoint

GET /api/clinic_patients/:clinic_patient_id/longitudinal_health_record

Purpose: Returns complete longitudinal health data for the frontend LHR dashboard

Controller: PatientsLhrController#show

Note: The frontend route uses clinic_patient_id (21) which needs to be converted to patient_id (23) in the controller

Expected Response Structure:

{
  "patient_id": "23318",
  "patient_info": {
    "id": "23318",
    "name": "John Smith",
    "age": 45,
    "gender": "Male",
    "phone": "+1-555-0234", 
    "email": "john.smith@example.com",
    "medical_id": "MR-001234",
    "allergies": ["Penicillin", "Peanuts"],
    "blood_group": "O+",
    "chronic_conditions": ["Hypertension", "Type 2 Diabetes"]
  },
  "encounters": [
    {
      "id": "enc-001",
      "date": "2024-01-15T10:00:00Z",
      "type": "follow-up",
      "provider": "Dr. Sarah Johnson",
      "chief_complaint": "Routine diabetes follow-up",
      "diagnosis": ["Type 2 Diabetes", "Hypertension"],
      "status": "completed",
      "has_notes": true,
      "has_prescriptions": true,
      "has_lab_orders": true,
      "vitals": [
        {
          "type": "blood_pressure",
          "value": "120/80",
          "unit": "mmHg",
          "date": "2024-01-15T10:00:00Z",
          "is_abnormal": false
        }
      ],
      "medications": [...],
      "problems": [...],
      "lab_results": [...]
    }
  ],
  "clinical_assessments": [
    {
      "id": "ci-001",
      "assessment_date": "2024-01-15T10:00:00Z",
      "summary": "Chief Complaints: Fatigue, increased thirst\\n\\nClinical Findings: Blood glucose elevated\\n\\nAssessment: Type 2 Diabetes, uncontrolled",
      "finding_codes": ["E11.9", "I10"],
      "finding_display": ["Type 2 Diabetes", "Hypertension"],
      "clinical_notes": "Patient reports poor medication adherence",
      "status": "completed",
      "doctor": "Dr. Sarah Johnson",
      "prescription_id": "presc-001"
    }
  ],
  "current_medications": [
    {
      "id": "med-001",
      "drug_name": "Metformin",
      "generic_name": "Metformin",
      "dosage": "500mg",
      "frequency": "BID",
      "duration": "Ongoing",
      "route": "PO",
      "instructions": "Take with meals",
      "start_date": "2023-06-01T00:00:00Z",
      "prescribed_by": "Dr. Sarah Johnson",
      "is_controlled": false,
      "interactions": [],
      "prescription_status": "active",
      "adherence_rate": 95,
      "refills_remaining": 2,
      "next_refill_date": "2024-02-15T00:00:00Z"
    }
  ],
  "problem_list": [
    {
      "id": "prob-001",
      "name": "Type 2 Diabetes Mellitus",
      "icd_code": "E11.9",
      "status": "chronic",
      "severity": "medium",
      "onset_date": "2020-03-15T00:00:00Z",
      "last_review_date": "2024-01-15T00:00:00Z",
      "linked_medications": ["med-001"],
      "linked_encounters": ["enc-001"],
      "notes": "Well controlled with medication"
    }
  ],
  "vitals_trends": [
    {
      "metric": "blood_pressure",
      "unit": "mmHg",
      "data_points": [
        {
          "date": "2024-01-15T10:00:00Z",
          "value": { "systolic": 120, "diastolic": 80 },
          "encounter_id": "enc-001",
          "is_abnormal": false,
          "notes": "Well controlled"
        }
      ],
      "normal_range": { "min": 90, "max": 140 },
      "latest_value": { "systolic": 120, "diastolic": 80 },
      "trend": "stable"
    }
  ],
  "medication_history": [
    {
      "id": "medhist-001",
      "medication_id": "med-001",
      "date": "2024-01-15T10:00:00Z",
      "type": "started",
      "drug_name": "Metformin",
      "dosage": "500mg",
      "frequency": "BID",
      "reason": "Diabetes management",
      "prescribed_by": "Dr. Sarah Johnson",
      "encounter_id": "enc-001"
    }
  ],
  "lab_results": [
    {
      "id": "lab-001",
      "test_name": "HbA1c",
      "test_code": "4548-4",
      "date": "2024-01-10T00:00:00Z",
      "value": "7.2",
      "unit": "%",
      "normal_range": "< 7.0",
      "status": "abnormal",
      "trend": "improving",
      "encounter_id": "enc-001",
      "ordered_by": "Dr. Sarah Johnson"
    }
  ],
  "clinical_assessments": [
    {
      "id": "assess-001",
      "encounter_id": "enc-001",
      "assessment_date": "2024-01-15T10:00:00Z",
      "assessment_type": "routine_followup",
      "provider": "Dr. Sarah Johnson",
      "subjective": "Patient reports feeling well, no new symptoms",
      "objective": "Vital signs stable, no acute distress",
      "assessment": "Diabetes well controlled, continue current regimen",
      "plan": "Continue metformin, follow up in 3 months",
      "priority": "routine",
      "follow_up_required": true,
      "follow_up_date": "2024-04-15T10:00:00Z"
    }
  ],
  "last_visit_date": "2024-01-15T10:00:00Z",
  "next_appointment_date": "2024-04-15T10:00:00Z",
  "active_problems_count": 3,
  "current_medications_count": 2,
  "total_encounters": 12
}

Implementation Strategy

Controller Implementation

# parts/patient/controllers/patients_lhr_controller.rb
class PatientsLhrController < ApiController
  before_action :set_clinic_patient
  
  def show
    result = Patients::GetLongitudinalHealthRecord.call(
      patient_id: params[:patient_id],
      clinic_id: current_user.clinic_id
    )
    render_result(result, result.success? ? :longitudinal_data : nil)
  end
  
  def problems
    result = PatientProblems::Index.call(
      patient_id: params[:patient_id],
      clinic_id: current_user.clinic_id
    )
    render_result(result, result.success? ? PatientProblemSerializer : nil)
  end
  
  def medications_timeline
    result = MedicationRequests::Timeline.call(
      patient_id: params[:patient_id],
      clinic_id: current_user.clinic_id
    )
    render_result(result, result.success? ? MedicationEventSerializer : nil)
  end
  
  def vitals_trends
    result = Patients::GetVitalsTrends.call(
      patient_id: params[:patient_id],
      clinic_id: current_user.clinic_id,
      date_range: params[:date_range]
    )
    render_result(result, result.success? ? VitalsTrendSerializer : nil)
  end
  
  private
  
  def set_clinic_patient
    @clinic_patient = ClinicPatient.find_by!(
      clinic_id: current_user.clinic_id,
      patient_id: params[:patient_id]
    )
  rescue ActiveRecord::RecordNotFound
    render json: { error: 'Patient not found in clinic' }, status: :not_found
  end
end

Main Interactor Implementation

# parts/patient/interactors/patients/get_longitudinal_health_record.rb
class Patients::GetLongitudinalHealthRecord < BaseInteractor
  def call
    validate_access
    gather_patient_data
    build_encounters_history
    aggregate_current_state
    aggregate_historical_trends
    calculate_summary_metrics
    build_response
  end
  
  private
  
  def validate_access
    context.clinic_patient = ClinicPatient.find_by!(
      clinic_id: current_user.clinic_id,
      patient_id: context.patient_id
    )
    context.patient = context.clinic_patient.patient
    authorize(context.patient, :show?)
  end
  
  def gather_patient_data
    context.patient_info = {
      id: context.patient.id.to_s,
      name: "#{context.patient.first_name} #{context.patient.last_name}".strip,
      age: context.patient.age || calculate_age(context.patient.date_of_birth),
      gender: context.patient.gender&.capitalize || 'Unknown',
      phone: context.patient.phone_number,
      email: context.patient.email,
      medical_id: context.clinic_patient.patient_code || "MR-#{context.patient.id}",
      allergies: context.patient.allergies || [],
      blood_group: context.patient.blood_group,
      chronic_conditions: extract_chronic_conditions
    }
  end
  
  def build_encounters_history
    appointments = Appointment.includes(
      :doctor, :clinic_doctor, :prescription => [
        :prescription_complaints, 
        :prescription_medicines, 
        :prescription_test_lists
      ]
    ).where(clinic_patient_id: context.clinic_patient.id)
     .order(appointment_date: :desc)
     .limit(50)
    
    context.encounters = appointments.map do |appointment|
      prescription = appointment.prescription
      doctor_name = "Dr. #{appointment.doctor.first_name} #{appointment.doctor.last_name}"
      
      {
        id: appointment.id.to_s,
        date: appointment.appointment_date.in_time_zone,
        type: map_appointment_type(appointment.appointment_type),
        provider: doctor_name,
        chief_complaint: prescription&.prescription_complaints&.first&.name || appointment.chief_complaint || '',
        diagnosis: prescription&.diagnosis || [],
        status: appointment.status == 'completed' ? 'completed' : 'in-progress',
        has_notes: prescription&.notes.present? || appointment.notes.present?,
        has_prescriptions: prescription&.prescription_medicines&.any?,
        has_lab_orders: prescription&.prescription_test_lists&.any?,
        vitals: extract_vitals_from_appointment(appointment),
        medications: extract_medications_from_prescription(prescription),
        problems: extract_problems_from_prescription(prescription),
        lab_results: extract_lab_results_from_prescription(prescription)
      }
    end
  end
  
  def aggregate_current_state
    # Current medications from patient_medicines
    current_meds = PatientMedicine.where(
      patient_id: context.patient_id,
      active: true
    ).includes(:prescription)
    
    context.current_medications = current_meds.map do |med|
      prescription = med.prescription
      doctor_name = "Dr. #{prescription.doctor.first_name} #{prescription.doctor.last_name}"
      
      {
        id: med.id.to_s,
        drug_name: med.medicine_name,
        generic_name: med.medicine_name, # Could be enhanced with drug database
        dosage: med.dosage,
        frequency: med.frequency,
        duration: med.duration || 'Ongoing',
        route: 'PO', # Could be enhanced with route tracking
        instructions: med.instructions,
        start_date: med.created_at,
        last_modified: med.updated_at,
        prescribed_by: doctor_name,
        is_controlled: false, # Could be enhanced with controlled substance tracking
        interactions: [], # Could be enhanced with drug interaction checking
        prescription_status: med.active? ? 'active' : 'completed',
        adherence_rate: calculate_adherence_rate(med),
        refills_remaining: med.refills_remaining,
        next_refill_date: calculate_next_refill_date(med)
      }
    end
    
    # Current problems (to be created from historical diagnoses)
    context.problem_list = extract_patient_problems
  end
  
  def aggregate_historical_trends
    # Vitals trends from appointment history
    context.vitals_trends = build_vitals_trends
    
    # Medication history from prescription changes
    context.medication_history = build_medication_history
    
    # Lab results from all encounters
    context.lab_results = build_lab_results_summary
    
    # Clinical assessments from encounters
    context.clinical_assessments = build_clinical_assessments
  end
  
  def calculate_summary_metrics
    context.last_visit_date = context.encounters.first&.dig(:date)
    context.next_appointment_date = get_next_appointment
    context.active_problems_count = context.problem_list.count { |p| p[:status].in?(['active', 'chronic']) }
    context.current_medications_count = context.current_medications.count
    context.total_encounters = context.encounters.count
  end
  
  def build_response
    context.longitudinal_data = {
      patient_id: context.patient_id.to_s,
      patient_info: context.patient_info,
      encounters: context.encounters,
      current_medications: context.current_medications,
      problem_list: context.problem_list,
      vitals_trends: context.vitals_trends,
      medication_history: context.medication_history,
      lab_results: context.lab_results,
      clinical_assessments: context.clinical_assessments,
      last_visit_date: context.last_visit_date,
      next_appointment_date: context.next_appointment_date,
      active_problems_count: context.active_problems_count,
      current_medications_count: context.current_medications_count,
      total_encounters: context.total_encounters
    }
  end
  
  # Helper methods
  
  def extract_chronic_conditions
    # Extract from patient medical history or aggregate from diagnoses
    conditions = []
    
    # From patient medical history
    if context.patient.medical_history.present?
      chronic_conditions = context.patient.medical_history.dig('chronic_conditions')
      conditions.concat(chronic_conditions) if chronic_conditions.is_a?(Array)
    end
    
    # From recent diagnoses (last 2 years)
    recent_diagnoses = Prescription.joins(:clinic_patient)
                                 .where(clinic_patients: { patient_id: context.patient_id })
                                 .where('prescriptions.created_at > ?', 2.years.ago)
                                 .where.not(diagnosis: [nil, []])
                                 .pluck(:diagnosis)
                                 .flatten
                                 .uniq
    
    conditions.concat(recent_diagnoses)
    conditions.uniq.compact
  end
  
  def extract_vitals_from_appointment(appointment)
    return [] unless appointment.vitals.present?
    
    vitals_data = appointment.vitals.is_a?(String) ? JSON.parse(appointment.vitals) : appointment.vitals
    
    vitals_data.map do |key, value|
      next unless value.present?
      
      {
        type: map_vital_type(key),
        value: value,
        unit: get_vital_unit(key),
        date: appointment.appointment_date,
        is_abnormal: is_vital_abnormal?(key, value)
      }
    end.compact
  end
  
  def extract_problems_from_prescription(prescription)
    return [] unless prescription&.prescription_complaints&.any?
    
    prescription.prescription_complaints.map do |complaint|
      {
        id: "comp-#{complaint.id}",
        name: complaint.name,
        status: 'active',
        severity: 'medium', # Could be enhanced with severity tracking
        onset_date: prescription.created_at,
        last_review_date: prescription.updated_at,
        linked_medications: [],
        linked_encounters: [prescription.event_id.to_s],
        notes: "Chief complaint from encounter"
      }
    end
  end
  
  def build_vitals_trends
    appointments = Appointment.where(clinic_patient_id: context.clinic_patient.id)
                             .where.not(vitals: [nil, '{}', ''])
                             .order(:appointment_date)
    
    trends_map = {}
    
    appointments.each do |appointment|
      vitals_data = appointment.vitals.is_a?(String) ? JSON.parse(appointment.vitals) : appointment.vitals
      
      vitals_data.each do |key, value|
        next unless value.present?
        
        vital_type = map_vital_type(key)
        next unless vital_type
        
        trends_map[vital_type] ||= {
          metric: vital_type,
          unit: get_vital_unit(key),
          data_points: []
        }
        
        trends_map[vital_type][:data_points] << {
          date: appointment.appointment_date,
          value: parse_vital_value(key, value),
          encounter_id: appointment.id.to_s,
          is_abnormal: is_vital_abnormal?(key, value)
        }
      end
    end
    
    # Add normal ranges and trends
    trends_map.values.map do |trend|
      trend[:normal_range] = get_normal_range(trend[:metric])
      trend[:latest_value] = trend[:data_points].last&.dig(:value)
      trend[:trend] = calculate_vital_trend(trend[:data_points])
      trend
    end
  end
  
  def build_medication_history
    # Build from prescription_medicines across all prescriptions
    prescriptions = Prescription.joins(:clinic_patient)
                               .includes(:prescription_medicines, :doctor)
                               .where(clinic_patients: { patient_id: context.patient_id })
                               .where(is_final: true)
                               .order(:created_at)
    
    events = []
    medication_tracker = {}
    
    prescriptions.each do |prescription|
      doctor_name = "Dr. #{prescription.doctor.first_name} #{prescription.doctor.last_name}"
      
      prescription.prescription_medicines.each do |medicine|
        med_key = medicine.medicine_name.downcase
        previous_med = medication_tracker[med_key]
        
        event_type = if previous_med.nil?
                      'started'
                    elsif previous_med[:dosage] != medicine.dosage
                      'dosageChanged'
                    elsif previous_med[:frequency] != medicine.frequency
                      'frequencyChanged'
                    else
                      'continued'
                    end
        
        next if event_type == 'continued' # Don't log if no changes
        
        events << {
          id: "medhist-#{prescription.id}-#{medicine.id}",
          medication_id: medicine.id.to_s,
          date: prescription.created_at,
          type: event_type,
          drug_name: medicine.medicine_name,
          dosage: medicine.dosage,
          previous_dosage: previous_med&.dig(:dosage),
          frequency: medicine.frequency,
          previous_frequency: previous_med&.dig(:frequency),
          reason: extract_medication_reason(medicine, prescription),
          prescribed_by: doctor_name,
          encounter_id: prescription.event_id&.to_s
        }
        
        medication_tracker[med_key] = {
          dosage: medicine.dosage,
          frequency: medicine.frequency
        }
      end
    end
    
    events.reverse # Newest first
  end
  
  def build_lab_results_summary
    # Build from prescription_test_lists and actual lab results if available
    prescriptions = Prescription.joins(:clinic_patient)
                               .includes(:prescription_test_lists => :prescription_tests, :doctor)
                               .where(clinic_patients: { patient_id: context.patient_id })
                               .where(is_final: true)
                               .order(created_at: :desc)
    
    results = []
    
    prescriptions.each do |prescription|
      doctor_name = "Dr. #{prescription.doctor.first_name} #{prescription.doctor.last_name}"
      
      prescription.prescription_test_lists.each do |test_list|
        test_list.prescription_tests.each do |test|
          results << {
            id: "lab-#{prescription.id}-#{test.id}",
            test_name: test.test_name,
            test_code: test.test_code,
            date: prescription.created_at,
            value: "Ordered", # Could be enhanced with actual results
            unit: "",
            normal_range: "",
            status: "normal", # Could be enhanced with actual results
            trend: "stable",
            encounter_id: prescription.event_id&.to_s,
            ordered_by: doctor_name
          }
        end
      end
    end
    
    results
  end
  
  # Helper methods for mapping data
  
  def map_appointment_type(type)
    case type&.downcase
    when 'consultation' then 'consultation'
    when 'followup', 'follow_up' then 'follow-up'
    when 'emergency' then 'emergency'
    when 'routine' then 'routine'
    else 'consultation'
    end
  end
  
  def map_vital_type(key)
    case key.to_s.downcase
    when 'bp', 'blood_pressure', 'bloodpressure' then 'blood_pressure'
    when 'hr', 'heart_rate', 'heartrate', 'pulse' then 'heart_rate'
    when 'temp', 'temperature' then 'temperature'
    when 'weight', 'wt' then 'weight'
    when 'spo2', 'oxygen_saturation', 'o2sat' then 'oxygen_saturation'
    when 'rr', 'respiratory_rate', 'resp_rate' then 'respiratory_rate'
    else nil
    end
  end
  
  def get_vital_unit(key)
    case map_vital_type(key)
    when 'blood_pressure' then 'mmHg'
    when 'heart_rate' then 'bpm'
    when 'temperature' then '°C'
    when 'weight' then 'kg'
    when 'oxygen_saturation' then '%'
    when 'respiratory_rate' then 'breaths/min'
    else ''
    end
  end
  
  def parse_vital_value(key, value)
    if map_vital_type(key) == 'blood_pressure' && value.to_s.include?('/')
      systolic, diastolic = value.to_s.split('/').map(&:to_i)
      { systolic: systolic, diastolic: diastolic }
    else
      value.to_f
    end
  end
  
  def is_vital_abnormal?(key, value)
    # Basic abnormal ranges - could be enhanced with age/gender specific ranges
    case map_vital_type(key)
    when 'blood_pressure'
      if value.to_s.include?('/')
        sys, dia = value.to_s.split('/').map(&:to_i)
        sys > 140 || sys < 90 || dia > 90 || dia < 60
      else
        false
      end
    when 'heart_rate'
      val = value.to_f
      val > 100 || val < 60
    when 'temperature'
      val = value.to_f
      val > 37.5 || val < 36
    when 'oxygen_saturation'
      value.to_f < 95
    else
      false
    end
  end
  
  def extract_patient_problems
    # Extract from historical diagnoses and create patient-level problems
    diagnoses_map = {}
    
    prescriptions = Prescription.joins(:clinic_patient)
                               .where(clinic_patients: { patient_id: context.patient_id })
                               .where.not(diagnosis: [nil, []])
                               .order(:created_at)
    
    prescriptions.each do |prescription|
      prescription.diagnosis.each do |diagnosis|
        diagnosis_key = diagnosis.downcase.strip
        
        if diagnoses_map[diagnosis_key]
          # Update existing problem
          diagnoses_map[diagnosis_key][:last_review_date] = prescription.created_at
          diagnoses_map[diagnosis_key][:linked_encounters] << prescription.event_id.to_s
        else
          # Create new problem
          diagnoses_map[diagnosis_key] = {
            id: "prob-#{Digest::MD5.hexdigest(diagnosis_key)}",
            name: diagnosis,
            status: determine_problem_status(diagnosis),
            severity: determine_problem_severity(diagnosis),
            onset_date: prescription.created_at,
            last_review_date: prescription.created_at,
            linked_medications: [],
            linked_encounters: [prescription.event_id.to_s],
            notes: "Identified from clinical encounters"
          }
        end
      end
    end
    
    # Link medications to problems
    diagnoses_map.values.each do |problem|
      linked_meds = context.current_medications.select do |med|
        problem[:name].downcase.include?(extract_condition_from_medication(med[:drug_name]))
      end
      problem[:linked_medications] = linked_meds.map { |med| med[:id] }
    end
    
    diagnoses_map.values
  end
  
  def determine_problem_status(diagnosis)
    chronic_keywords = ['diabetes', 'hypertension', 'copd', 'asthma', 'arthritis']
    
    if chronic_keywords.any? { |keyword| diagnosis.downcase.include?(keyword) }
      'chronic'
    else
      'active'
    end
  end
  
  def determine_problem_severity(diagnosis)
    critical_keywords = ['heart attack', 'stroke', 'cancer', 'emergency']
    high_keywords = ['diabetes', 'hypertension', 'copd']
    
    if critical_keywords.any? { |keyword| diagnosis.downcase.include?(keyword) }
      'critical'
    elsif high_keywords.any? { |keyword| diagnosis.downcase.include?(keyword) }
      'high'
    else
      'medium'
    end
  end
end

Routes Configuration

# parts/patient/routes.rb
Rails.application.routes.draw do
  namespace :api do
    resources :patients do
      member do
        get :longitudinal_health_record, to: 'patients_lhr#show'
        get :problems, to: 'patients_lhr#problems'
        get :medications_timeline, to: 'patients_lhr#medications_timeline'
        get :vitals_trends, to: 'patients_lhr#vitals_trends'
      end
    end
  end
end

Data Migration Rake Tasks

# lib/tasks/lhr_migration.rake
namespace :lhr do
  desc "Migrate existing prescription data to support LHR"
  task migrate_for_lhr: :environment do
    puts "Starting LHR migration..."
    
    # 1. Create patient problems from historical diagnoses
    migrate_patient_problems
    
    # 2. Enhance vitals data structure
    standardize_vitals_data
    
    # 3. Create medication history events
    create_medication_history
    
    puts "LHR migration completed!"
  end
  
  def migrate_patient_problems
    puts "Creating patient problems from historical diagnoses..."
    
    problems_created = 0
    
    ClinicPatient.includes(:patient, prescriptions: :doctor).find_each do |clinic_patient|
      patient = clinic_patient.patient
      clinic_id = clinic_patient.clinic_id
      
      diagnoses_map = {}
      
      clinic_patient.prescriptions.where.not(diagnosis: [nil, []]).order(:created_at).each do |prescription|
        prescription.diagnosis.each do |diagnosis|
          diagnosis_key = diagnosis.downcase.strip
          
          unless diagnoses_map[diagnosis_key]
            diagnoses_map[diagnosis_key] = {
              clinic_id: clinic_id,
              patient_id: patient.id,
              doctor_id: prescription.doctor_id,
              problem_name: diagnosis,
              problem_category: categorize_diagnosis(diagnosis),
              status: determine_problem_status(diagnosis),
              severity: determine_problem_severity(diagnosis),
              onset_date: prescription.prescription_date || prescription.created_at.to_date,
              first_encounter_id: prescription.event_id,
              last_encounter_id: prescription.event_id,
              clinical_notes: "Migrated from prescription diagnoses"
            }
          else
            # Update last encounter
            diagnoses_map[diagnosis_key][:last_encounter_id] = prescription.event_id
          end
        end
      end
      
      # Create patient problems
      diagnoses_map.values.each do |problem_data|
        begin
          PatientProblem.create!(problem_data)
          problems_created += 1
        rescue ActiveRecord::RecordInvalid => e
          puts "Failed to create problem: #{problem_data[:problem_name]} - #{e.message}"
        end
      end
    end
    
    puts "Created #{problems_created} patient problems"
  end
  
  def standardize_vitals_data
    puts "Standardizing vitals data format..."
    
    updated_count = 0
    
    Appointment.where.not(vitals: [nil, '{}', '']).find_each do |appointment|
      begin
        vitals_data = appointment.vitals.is_a?(String) ? JSON.parse(appointment.vitals) : appointment.vitals
        standardized = {}
        
        vitals_data.each do |key, value|
          standardized_key = standardize_vital_key(key)
          standardized[standardized_key] = value if standardized_key
        end
        
        if standardized.any?
          appointment.update!(vitals: standardized)
          updated_count += 1
        end
      rescue JSON::ParserError => e
        puts "Failed to parse vitals for appointment #{appointment.id}: #{e.message}"
      end
    end
    
    puts "Standardized vitals for #{updated_count} appointments"
  end
  
  def create_medication_history
    puts "Creating medication history from prescriptions..."
    
    # This will be handled by background jobs when prescriptions are finalized
    # For now, create history for existing finalized prescriptions
    
    count = 0
    Prescription.where(is_final: true).includes(:prescription_medicines).find_each do |prescription|
      prescription.prescription_medicines.each do |medicine|
        # Create medication request if not exists
        unless MedicationRequest.exists?(
          prescription_id: prescription.id,
          medication_name: medicine.medicine_name
        )
          MedicationRequest.create!(
            clinic_id: prescription.clinic_id,
            patient_id: prescription.patient_id,
            doctor_id: prescription.doctor_id,
            encounter_id: prescription.event_id,
            prescription_id: prescription.id,
            medication_name: medicine.medicine_name,
            dosage_instruction: "#{medicine.dosage} #{medicine.frequency}",
            frequency: medicine.frequency,
            instructions: medicine.instructions,
            status: 'completed'
          )
          count += 1
        end
      end
    end
    
    puts "Created #{count} medication requests from historical prescriptions"
  end
  
  # Helper methods
  def categorize_diagnosis(diagnosis)
    case diagnosis.downcase
    when /diabetes/ then 'Endocrine'
    when /hypertension|blood pressure/ then 'Cardiovascular'
    when /infection|fever/ then 'Infectious'
    when /pain|arthritis/ then 'Musculoskeletal'
    else 'General'
    end
  end
  
  def standardize_vital_key(key)
    case key.to_s.downcase
    when 'bp', 'blood_pressure', 'bloodpressure' then 'blood_pressure'
    when 'hr', 'heart_rate', 'heartrate', 'pulse' then 'heart_rate'
    when 'temp', 'temperature' then 'temperature'
    when 'weight', 'wt' then 'weight'
    when 'spo2', 'oxygen_saturation', 'o2sat' then 'oxygen_saturation'
    when 'rr', 'respiratory_rate', 'resp_rate' then 'respiratory_rate'
    else nil
    end
  end
end

This creates the exact backend structure needed to support your existing LHR frontend at /cms/careplan/patient/:patient_id.

<function_calls> [{"content": "Analyze existing LHR frontend URL structure and components", "status": "completed", "priority": "high", "id": "1"}, {"content": "Create backend endpoints for LHR UI integration", "status": "completed", "priority": "high", "id": "2"}, {"content": "Update documentation with endpoint specifications", "status": "completed", "priority": "high", "id": "3"}]

On this page