Kulcare Docs
DeveloperCare plansLhr

API Design for Longitudinal Health Records

Overview

This document outlines the API endpoints and controller enhancements required for the LHR system. The design maintains 100% backward compatibility while adding powerful new longitudinal health record capabilities.

Core Principle: Enhance existing APIs with LHR data, add new endpoints for LHR-specific features, ensure all existing functionality continues to work unchanged.

Enhanced Existing APIs

1. Enhanced Encounter APIs

Enhanced Encounter Index

# GET /api/encounters
# Enhanced response to include LHR summary data

{
  "encounters": [
    {
      "id": 123,
      "patient_id": 456,
      "doctor_id": 789,
      # ... existing fields unchanged ...
      
      # NEW: LHR enhancement fields (optional, only if available)
      "encounter_type": "routine",
      "health_score_snapshot": 78.5,
      # Note: reconciliation_completed removed - using simple continuity,
      "lhr_enhanced": true,
      "enhanced_complaints_count": 3,
      "enhanced_medicines_count": 2,
      "clinical_alerts_count": 1
    }
  ],
  # NEW: Optional LHR summary for the patient list
  "lhr_summary": {
    "total_encounters": 15,
    "avg_health_score": 76.2,
    "patients_with_alerts": 3,
    "chronic_medications_count": 12
  }
}

Enhanced Encounter Show

# GET /api/encounters/:id?include_lhr=true
# Enhanced response with comprehensive LHR data

{
  "encounter": {
    "id": 123,
    # ... existing fields unchanged ...
    
    # NEW: LHR fields
    "encounter_type": "routine",
    "health_score_snapshot": 78.5,
    "reconciliation_completed": false,
    
    # Enhanced complaints with longitudinal data
    "complaints": [
      {
        "id": 1,
        "name": "Hypertension",
        # ... existing fields ...
        
        # NEW: LHR fields
        "problem_status": "chronic",
        "severity_level": "moderate",
        "onset_date": "2023-06-15",
        "chronic_condition": true,
        "linked_complaint_ids": [45, 67],
        "affects_health_score": true,
        "clinical_codes": {
          "primary": {
            "system": "ICD10",
            "code": "I10",
            "display": "Essential hypertension"
          },
          "confidence": 0.95
        }
      }
    ],
    
    # Enhanced medicines with continuity data
    "medicines": [
      {
        "id": 1,
        "medicine_name": "Lisinopril",
        # ... existing fields ...
        
        # NEW: LHR fields
        "medication_status": "active",
        "prescription_type": "chronic",
        "start_date": "2023-06-15",
        "end_date": null,
        "drug_interactions": [
          {
            "severity": "moderate",
            "description": "Monitor potassium levels",
            "interacting_drug": "Potassium supplements"
          }
        ],
        # Note: reconciliation_status removed,
        "rxnorm_code": {
          "code": "29046",
          "display": "Lisinopril"
        }
      }
    ]
  },
  
  # NEW: LHR insights (only when include_lhr=true)
  "lhr_insights": {
    "health_score_trend": "improving",
    "clinical_alerts": [
      {
        "type": "drug_interaction",
        "severity": "moderate",
        "message": "Potential drug interactions detected",
        "count": 1
      }
    ],
    "patient_summary": {
      "active_problems_count": 2,
      "chronic_conditions_count": 1,
      "current_medications_count": 3,
      "last_health_score": 78.5,
      "health_score_date": "2024-01-15"
    }
  }
}

Enhanced Encounter Update

# PATCH /api/encounters/:id
# Existing functionality preserved, new LHR fields can be updated

# Request body (backward compatible)
{
  "encounter": {
    # ... existing fields work as before ...
    
    # NEW: UI-driven fields from encounter creation
    "clinic_patient_id": 456,
    "doctor_id": 789,
    "visit_type": "scheduled",  # walkin/scheduled/telemedicine/home_visit
    "urgency": "routine",       # routine/urgent/emergency
    "chief_complaint": "Follow-up for hypertension management",
    "encounter_type": "followup", # routine/emergency/followup/telemedicine
    
    # NEW: Optional LHR fields
    # Note: reconciliation_completed removed,
    
    # Enhanced complaint updates
    "complaints": [
      {
        "id": 1,
        # ... existing fields ...
        "name": "Hypertension",
        "problem_status": "chronic",
        "severity_level": "moderate",
        "onset_date": "2023-06-15",
        "chronic_condition": true
      }
    ],
    
    # Enhanced medicine updates  
    "medicines": [
      {
        "id": 1,
        # ... existing fields ...
        "medication_status": "active",
        "prescription_type": "chronic",
        "reconciliation_status": "verified"
      }
    ]
  }
}

# Response includes LHR enhancements
{
  "success": true,
  "encounter": { /* enhanced encounter data */ },
  "lhr_processing_queued": true  # Background LHR enhancement queued
}

New LHR-Specific APIs

2. Longitudinal Health Summary APIs

Get Patient Health Summary

# GET /api/patients/:patient_id/health_summary
# Comprehensive longitudinal health overview with UI-friendly structure

{
  "patient_id": 456,
  "generated_at": "2024-01-15T10:30:00Z",
  "patient_demographics": {
    "name": "John Doe",
    "age": 45,
    "gender": "Male",
    "date_of_birth": "1979-03-15",
    "phone": "+91-9876543210",
    "email": "john.doe@email.com",
    "address": "123 Main Street, Mumbai",
    "blood_group": "O+",
    "profile_picture": "/api/patients/456/photo"
  },
  "health_summary": {
    # Health Score Card
    "health_score_card": {
      "overall_score": 78.5,
      "trend": "improving",
      "components": {
        "vitals": { "score": 85, "label": "Vitals" },
        "problems": { "score": 70, "label": "Problems" },
        "medications": { "score": 82, "label": "Medications" },
        "preventive": { "score": 75, "label": "Preventive Care" }
      },
      "last_calculated": "2024-01-15T08:00:00Z"
    },
    
    # Recent Encounters Timeline
    "recent_encounters": [
      {
        "id": 123,
        "date": "2024-01-10",
        "visit_type": "scheduled",
        "urgency": "routine",
        "chief_complaint": "Follow-up for hypertension",
        "doctor": "Dr. Smith",
        "encounter_type": "followup",
        "summary": "BP controlled, continue current medications"
      }
    ],
    
    # Active Problems Card
    "active_problems": {
      "count": 2,
      "items": [
        {
          "name": "Essential Hypertension",
          "severity": "moderate",
          "onset_date": "2023-06-15",
          "duration": "6 months",
          "status": "chronic",
          "last_updated": "2024-01-10"
        }
      ]
    },
    
    # Current Medications Card
    "current_medications": {
      "count": 3,
      "items": [
        {
          "name": "Lisinopril",
          "dosage": "10mg",
          "frequency": "Once daily",
          "type": "chronic",
          "start_date": "2023-06-15",
          "prescriber": "Dr. Smith",
          "status": "active",
          "adherence": "good"
        }
      ]
    },
    
    # Vitals Card
    "recent_vitals": {
      "blood_pressure": {
        "value": "125/80",
        "unit": "mmHg",
        "date": "2024-01-10",
        "trend": "improving",
        "status": "normal"
      },
      "weight": {
        "value": "75",
        "unit": "kg",
        "date": "2024-01-10",
        "trend": "stable",
        "bmi": 24.2
      },
      "temperature": {
        "value": "98.6",
        "unit": "°F",
        "date": "2024-01-10",
        "status": "normal"
      }
    },
    
    # Summary Statistics
    "statistics": {
      "total_encounters": 15,
      "encounters_this_year": 4,
      "days_since_last_visit": 5,
      "upcoming_appointments": 1
    }
  }
}

Get Health Score History

# GET /api/patients/:patient_id/health_scores?period=6_months
# Health score trends over time

{
  "patient_id": 456,
  "period": "6_months",
  "health_scores": [
    {
      "overall_score": 78.5,
      "vital_score": 85.0,
      "problem_score": 70.0,
      "medication_score": 82.0,
      "preventive_score": 75.0,
      "calculated_date": "2024-01-15",
      "encounter_id": 123
    }
  ],
  "trends": {
    "overall_trend": "improving",
    "change_from_previous": "+5.2",
    "best_score": 78.5,
    "worst_score": 65.3
  }
}

3. Clinical Coding APIs

Search Clinical Codes

# GET /api/clinical_codes/search?q=hypertension&system=ICD10&category=diagnosis
# Search for clinical codes

{
  "query": "hypertension",
  "results": [
    {
      "id": 1,
      "code_system": "ICD10",
      "code": "I10",
      "display_name": "Essential hypertension",
      "description": "High blood pressure of unknown cause",
      "category": "diagnosis",
      "usage_count": 1250,
      "confidence": 0.95
    }
  ],
  "total_results": 15
}

Get Code Suggestions

# GET /api/clinical_codes/suggest?text=patient has high blood pressure&category=diagnosis
# AI-powered code suggestions

{
  "text": "patient has high blood pressure",
  "suggestions": [
    {
      "code": {
        "id": 1,
        "code_system": "ICD10", 
        "code": "I10",
        "display_name": "Essential hypertension"
      },
      "confidence": 0.92,
      "method": "ai_generated",
      "reasoning": "Text indicates elevated blood pressure without specified cause"
    }
  ]
}

4. AI Transcription APIs

Start Transcription Session

# POST /api/ai_transcription/start_session
# Begin AI transcription for an encounter

# Request
{
  "encounter_id": 123,
  "settings": {
    "auto_populate": true,
    "confidence_threshold": 0.8,
    "recording_quality": "high"
  }
}

# Response  
{
  "success": true,
  "session": {
    "session_id": "uuid-string",
    "encounter_id": 123,
    "status": "active",
    "started_at": "2024-01-15T10:00:00Z"
  }
}

Process Audio Chunk

# POST /api/ai_transcription/process_audio
# Send audio data for transcription

# Request
{
  "session_id": "uuid-string",
  "audio_data": "base64-encoded-audio",
  "format": "webm",
  "duration": 5
}

# Response
{
  "success": true,
  "transcription": {
    "id": 456,
    "status": "processing",
    "confidence": null
  }
}

Get Live Updates

# GET /api/ai_transcription/live_updates?session_id=uuid-string
# Real-time transcription and suggestions

{
  "session_id": "uuid-string",
  "status": "active",
  "live_transcript": "Patient reports chest pain for the past 2 days...",
  "live_suggestions": {
    "complaints": [
      {
        "text": "chest pain",
        "confidence": 0.95,
        "suggested_codes": ["R06.02"]
      }
    ],
    "medications": [
      {
        "name": "aspirin",
        "dosage": "81mg",
        "frequency": "daily",
        "confidence": 0.87
      }
    ]
  }
}

5. Medication Continuity APIs

Get Chronic Medications

# GET /api/encounters/:encounter_id/chronic_medications
# Get patient's chronic medications for continuity

{
  "encounter_id": 123,
  "patient_id": 456,
  "chronic_medications": [
    {
      "id": 789,
      "medicine_name": "Metformin",
      "generic_name": "Metformin",
      "dosage": "500mg",
      "frequency": "Twice daily",
      "status": "active",
      "prescription_type": "chronic",
      "start_date": "2023-01-15",
      "last_prescribed_date": "2023-12-01",
      "prescribed_by": "Dr. Johnson"
    }
  ],
  "summary": {
    "total_chronic_medications": 3,
    "active_chronic_medications": 3
  }
}

Continue Chronic Medications

# POST /api/encounters/:encounter_id/continue_medications
# Continue selected chronic medications in current encounter

# Request
{
  "encounter_id": 123,
  "medication_ids": [789, 790, 791]
}

# Response
{
  "success": true,
  "continued_count": 3,
  "medications": [
    {
      "id": 999,
      "medicine_name": "Metformin",
      "dosage": "500mg",
      "frequency": "Twice daily",
      "prescription_type": "chronic",
      "parent_prescription_id": 789
    }
  ]
}

Controller Implementation

Enhanced Encounters Controller

# parts/doctor/controllers/encounters_controller.rb (Enhanced)
class EncountersController < ApiController
  before_action :policy_check
  before_action :set_encounter, only: [:show, :update, :lhr_summary, :update_longitudinal]

  # EXISTING methods work unchanged
  def create
    result = Encounters::Create.call(params)
    render_result(result, enhanced_encounter_serializer(result.encounter))
  end

  def index
    result = Encounters::Index.call(params)
    encounters = result.encounters
    
    # Add LHR summary if requested
    response_data = { encounters: enhanced_encounter_serializer(encounters) }
    if params[:include_lhr_summary] == 'true'
      response_data[:lhr_summary] = generate_lhr_summary_for_list(encounters)
    end
    
    render json: response_data
  end

  def show
    # Enhanced show with optional LHR data
    serializer_options = {
      include_lhr: params[:include_lhr] == 'true'
    }
    
    response_data = { encounter: enhanced_encounter_serializer(@encounter, serializer_options) }
    
    if serializer_options[:include_lhr]
      response_data[:lhr_insights] = generate_lhr_insights(@encounter)
    end
    
    render json: response_data
  end

  def update
    result = Encounters::Update.call(params.merge(encounter: @encounter))
    
    # Queue LHR enhancement if new data was added
    if result.success? && has_lhr_updates?(params)
      LhrEnhancementJob.perform_later(@encounter.id, 'ENCOUNTER_UPDATED')
    end
    
    render_result(result, enhanced_encounter_serializer(result.encounter))
  end

  # NEW: Get comprehensive LHR summary for an encounter
  def lhr_summary
    summary = {
      health_score: @encounter.health_score_snapshot,
      encounter_type: @encounter.encounter_type,
      enhanced_complaints: @encounter.lhr_enhanced_complaints.count,
      enhanced_medicines: @encounter.lhr_enhanced_medicines.count,
      # Note: reconciliation_completed removed,
      clinical_alerts: @encounter.clinical_alerts,
      ai_transcription_available: @encounter.ai_transcriptions.completed.any?
    }
    
    render json: { lhr_summary: summary }
  end

  # NEW: Update only LHR-specific fields  
  def update_longitudinal
    allowed_fields = params.slice(:encounter_type).to_h
    
    if @encounter.update(allowed_fields)
      render json: { success: true, updated_fields: allowed_fields.keys }
    else
      render json: { errors: @encounter.errors.full_messages }, status: :unprocessable_entity
    end
  end

  private

  def enhanced_encounter_serializer(encounter_or_collection, options = {})
    if encounter_or_collection.respond_to?(:each)
      # Collection
      encounter_or_collection.map { |encounter| serialize_single_encounter(encounter, options) }
    else
      # Single encounter
      serialize_single_encounter(encounter_or_collection, options)
    end
  end

  def serialize_single_encounter(encounter, options = {})
    base_data = EncounterSerializer.new(encounter).as_json
    
    # Add LHR enhancements if available
    if encounter.has_lhr_enhancements?
      base_data.merge!(
        encounter_type: encounter.encounter_type,
        health_score_snapshot: encounter.health_score_snapshot,
        # Note: reconciliation_completed removed,
        lhr_enhanced: true
      )
      
      # Enhanced complaints
      base_data[:complaints] = encounter.encounter_complaints.map do |complaint|
        complaint_data = ComplaintSerializer.new(complaint).as_json
        if complaint.problem_status.present?
          complaint_data.merge!(
            problem_status: complaint.problem_status,
            severity_level: complaint.severity_level,
            onset_date: complaint.onset_date,
            chronic_condition: complaint.chronic_condition,
            clinical_codes: format_clinical_codes(complaint)
          )
        end
        complaint_data
      end
      
      # Enhanced medicines  
      base_data[:medicines] = encounter.encounter_medicines.map do |medicine|
        medicine_data = MedicineSerializer.new(medicine).as_json
        if medicine.medication_status.present?
          medicine_data.merge!(
            medication_status: medicine.medication_status,
            prescription_type: medicine.prescription_type,
            start_date: medicine.start_date,
            end_date: medicine.end_date,
            drug_interactions: medicine.drug_interactions,
            # Note: reconciliation_status removed
          )
        end
        medicine_data
      end
    end
    
    base_data
  end

  def generate_lhr_insights(encounter)
    {
      health_score_trend: encounter.health_score_trend,
      clinical_alerts: encounter.clinical_alerts,
      patient_summary: {
        active_problems_count: encounter.patient.encounter_complaints.where(problem_status: 'active').count,
        chronic_conditions_count: encounter.patient.encounter_complaints.where(chronic_condition: true).count,
        current_medications_count: encounter.patient.encounter_medicines.where(medication_status: 'active').count,
        last_health_score: encounter.patient.patient_health_scores.recent.first&.overall_score,
        health_score_date: encounter.patient.patient_health_scores.recent.first&.calculated_date
      }
    }
  end

  def has_lhr_updates?(params)
    lhr_fields = [:encounter_type]
    params.dig(:encounter)&.keys&.any? { |key| lhr_fields.include?(key.to_sym) }
  end
end

New LHR Controllers

# app/controllers/api/longitudinal_health_controller.rb
class Api::LongitudinalHealthController < ApiController
  before_action :authenticate_user!
  before_action :set_patient

  def health_summary
    service = LongitudinalHealthService.new(@patient.id)
    summary = service.build_complete_record
    
    render json: {
      patient_id: @patient.id,
      generated_at: Time.current,
      health_summary: summary
    }
  end

  def health_scores
    period = params[:period] || '6_months'
    scores = @patient.patient_health_scores
                   .where('calculated_date >= ?', parse_period(period))
                   .order(:calculated_date)
    
    render json: {
      patient_id: @patient.id,
      period: period,
      health_scores: scores.map { |score| serialize_health_score(score) },
      trends: calculate_score_trends(scores)
    }
  end

  private

  def set_patient
    @patient = Patient.find(params[:patient_id])
    authorize @patient, :show?
  end

  def parse_period(period)
    case period
    when '1_month' then 1.month.ago
    when '3_months' then 3.months.ago  
    when '6_months' then 6.months.ago
    when '1_year' then 1.year.ago
    else 6.months.ago
    end
  end
end

API Route Structure

# config/routes.rb (additions)
Rails.application.routes.draw do
  namespace :api, defaults: { format: :json } do
    # Enhanced encounter routes (backward compatible)
    resources :encounters do
      member do
        get :lhr_summary
        patch :update_longitudinal
      end
    end
    
    # New LHR-specific routes
    resources :patients do
      get :health_summary, controller: 'longitudinal_health'
      get :health_scores, controller: 'longitudinal_health'
    end
    
    # Clinical coding routes
    resources :clinical_codes, only: [:index, :show] do
      collection do
        get :search
        get :suggest
      end
    end
    
    # AI transcription routes
    namespace :ai_transcription do
      post :start_session
      post :process_audio
      get :live_updates
      post :end_session
    end
    
    # Medication continuity routes
    resources :encounters do
      get :chronic_medications
      post :continue_medications
    end
  end
end

Error Handling

Standard Error Responses

# LHR-specific error handling
class LhrErrorHandler
  def self.handle_lhr_error(error, context = {})
    case error
    when LhrEnhancementError
      {
        error: 'lhr_enhancement_failed',
        message: 'LHR enhancement failed but core functionality preserved',
        details: error.message,
        fallback_available: true
      }
    when ClinicalCodingError
      {
        error: 'clinical_coding_unavailable',
        message: 'Clinical coding service unavailable, manual coding required',
        fallback_mode: true
      }
    when AiTranscriptionError
      {
        error: 'ai_transcription_failed', 
        message: 'AI transcription unavailable, manual documentation required',
        manual_fallback: true
      }
    else
      {
        error: 'lhr_service_error',
        message: 'LHR service temporarily unavailable'
      }
    end
  end
end

API Versioning Strategy

# Support for API versioning as LHR features evolve
module Api
  module V1
    # Current API with LHR enhancements
    class EncountersController < BaseController
      # Enhanced implementation
    end
  end
  
  module V2
    # Future API version with additional LHR features
    class EncountersController < BaseController
      # Future enhancements
    end
  end
end

Benefits of This API Design:

✅ 100% Backward Compatibility: All existing API calls work unchanged
✅ Progressive Enhancement: LHR data appears when available
✅ Optional Features: Clients can opt into LHR features via parameters
✅ Performance Optimized: Heavy LHR processing happens in background
✅ Error Resilient: LHR failures don't break core functionality
✅ Future-Proof: Versioned APIs support evolution of LHR features

Next: See health-scoring.md for detailed health scoring algorithms.

On this page