Kulcare Docs
DeveloperCare plansCare planMigration

New LHR APIs Implementation Plan

Overview

This document outlines the implementation plan for new LHR (Longitudinal Health Record) APIs needed to support full interactive functionality in the frontend tabs. These APIs will extend the existing read-only LHR system to support CRUD operations for problems, medications, and encounters.

Current API Status

✅ Existing APIs (Read-Only)

  • GET /clinics/:clinic_id/patients/:clinic_patient_id/longitudinal_health_record
  • GET /clinics/:clinic_id/patients/:clinic_patient_id/problems
  • GET /clinics/:clinic_id/patients/:clinic_patient_id/medications_timeline
  • GET /clinics/:clinic_id/patients/:clinic_patient_id/clinical_assessments
  • GET /clinics/:clinic_id/patients/:clinic_patient_id/vitals_trends

New APIs Required

Phase 1: Patient Problems Management

1.1 Create Patient Problem

Endpoint: POST /clinics/:clinic_id/patients/:clinic_patient_id/problems

Request Body:

{
  "problem_name": "Type 2 Diabetes Mellitus",
  "problem_code": "E11.9",
  "problem_category": "Endocrine",
  "severity": "medium",
  "status": "chronic",
  "onset_date": "2024-01-15",
  "clinical_notes": "Well controlled with medication",
  "linked_medication_ids": ["med-001", "med-002"]
}

Response:

{
  "success": true,
  "problem": {
    "id": "prob-new-001",
    "problem_name": "Type 2 Diabetes Mellitus",
    "problem_code": "E11.9",
    "category": "Endocrine",
    "severity": "medium",
    "status": "chronic",
    "onset_date": "2024-01-15",
    "duration_days": 30,
    "is_chronic": true,
    "clinical_notes": "Well controlled with medication",
    "identified_by": "Dr. Sarah Johnson",
    "last_updated": "2024-02-15T10:00:00Z",
    "linked_medications": ["med-001", "med-002"]
  }
}

Controller: ClinicPatientsLhrController#create_problem Interactor: ClinicPatients::CreatePatientProblem

1.2 Update Patient Problem

Endpoint: PUT /clinics/:clinic_id/patients/:clinic_patient_id/problems/:problem_id

Request Body: Same as create (all fields optional for update)

Response: Updated problem object

Controller: ClinicPatientsLhrController#update_problem Interactor: ClinicPatients::UpdatePatientProblem

1.3 Resolve Patient Problem

Endpoint: PUT /clinics/:clinic_id/patients/:clinic_patient_id/problems/:problem_id/resolve

Request Body:

{
  "resolution_notes": "Patient reports symptoms resolved",
  "resolution_date": "2024-02-15"
}

Response: Updated problem with status "resolved"

Controller: ClinicPatientsLhrController#resolve_problem Interactor: ClinicPatients::ResolvePatientProblem

1.4 Delete Patient Problem

Endpoint: DELETE /clinics/:clinic_id/patients/:clinic_patient_id/problems/:problem_id

Response:

{
  "success": true,
  "message": "Problem deleted successfully"
}

Controller: ClinicPatientsLhrController#delete_problem Interactor: ClinicPatients::DeletePatientProblem

Endpoint: PUT /clinics/:clinic_id/patients/:clinic_patient_id/problems/:problem_id/medications

Request Body:

{
  "medication_ids": ["med-001", "med-003", "med-005"]
}

Response: Updated problem with linked medications

Controller: ClinicPatientsLhrController#link_medications Interactor: ClinicPatients::LinkProblemMedications

Phase 2: Medication Management

2.1 Get Medication Details

Endpoint: GET /clinics/:clinic_id/patients/:clinic_patient_id/medications/:medication_id

Response:

{
  "medication": {
    "id": "med-001",
    "drug_name": "Metformin",
    "generic_name": "Metformin",
    "dosage": "500mg",
    "frequency": "BID",
    "route": "PO",
    "instructions": "Take with meals",
    "start_date": "2023-06-01",
    "end_date": null,
    "status": "active",
    "prescribed_by": "Dr. Sarah Johnson",
    "prescription_id": "presc-001",
    "linked_problems": ["prob-001"],
    "adherence_rate": 95,
    "side_effects": [],
    "notes": "Patient tolerating well"
  }
}

Controller: ClinicPatientsLhrController#show_medication Interactor: ClinicPatients::GetMedicationDetails

2.2 Update Medication Status

Endpoint: PUT /clinics/:clinic_id/patients/:clinic_patient_id/medications/:medication_id/status

Request Body:

{
  "status": "discontinued",
  "reason": "Side effects",
  "effective_date": "2024-02-15",
  "notes": "Patient reported nausea"
}

Response: Updated medication with new status

Controller: ClinicPatientsLhrController#update_medication_status Interactor: ClinicPatients::UpdateMedicationStatus

Phase 3: Encounter Management

3.1 Get Encounter Details

Endpoint: GET /clinics/:clinic_id/patients/:clinic_patient_id/encounters/:encounter_id

Response:

{
  "encounter": {
    "id": "enc-001",
    "appointment_id": 12345,
    "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",
    "vitals": [
      {
        "type": "blood_pressure",
        "value": "120/80",
        "unit": "mmHg",
        "is_abnormal": false
      }
    ],
    "medications_prescribed": [
      {
        "drug_name": "Metformin",
        "dosage": "500mg",
        "frequency": "BID"
      }
    ],
    "lab_orders": [
      {
        "test_name": "HbA1c",
        "status": "ordered"
      }
    ],
    "problems_addressed": ["prob-001"],
    "clinical_notes": "Patient doing well, continue current regimen",
    "follow_up_required": true,
    "follow_up_date": "2024-04-15"
  }
}

Controller: ClinicPatientsLhrController#show_encounter Interactor: ClinicPatients::GetEncounterDetails

3.2 Schedule Follow-up Appointment

Endpoint: POST /clinics/:clinic_id/patients/:clinic_patient_id/encounters/:encounter_id/follow_up

Request Body:

{
  "follow_up_date": "2024-04-15T10:00:00Z",
  "appointment_type": "follow-up",
  "notes": "3-month diabetes follow-up",
  "doctor_id": 123
}

Response:

{
  "success": true,
  "appointment": {
    "id": 67890,
    "appointment_time": "2024-04-15T10:00:00Z",
    "status": "scheduled",
    "type": "follow-up"
  }
}

Controller: ClinicPatientsLhrController#schedule_follow_up Interactor: ClinicPatients::ScheduleFollowUpAppointment

Database Schema Requirements

New Tables

1. Enhanced PatientProblem table

-- Assuming PatientProblem table exists, add missing columns:
ALTER TABLE patient_problems ADD COLUMN IF NOT EXISTS problem_code VARCHAR(50);
ALTER TABLE patient_problems ADD COLUMN IF NOT EXISTS problem_category VARCHAR(100);
ALTER TABLE patient_problems ADD COLUMN IF NOT EXISTS resolution_date DATE;
ALTER TABLE patient_problems ADD COLUMN IF NOT EXISTS resolution_notes TEXT;
ALTER TABLE patient_problems ADD COLUMN IF NOT EXISTS duration_days INTEGER;
ALTER TABLE patient_problems ADD COLUMN IF NOT EXISTS linked_medications JSONB DEFAULT '[]';

2. MedicationStatusHistory table (new)

CREATE TABLE medication_status_history (
  id BIGSERIAL PRIMARY KEY,
  clinic_id BIGINT NOT NULL,
  patient_id BIGINT NOT NULL,
  patient_medicine_id BIGINT NOT NULL,
  previous_status VARCHAR(50),
  new_status VARCHAR(50) NOT NULL,
  reason TEXT,
  effective_date DATE NOT NULL,
  notes TEXT,
  changed_by_user_id BIGINT NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  
  FOREIGN KEY (clinic_id) REFERENCES clinics(id),
  FOREIGN KEY (patient_id) REFERENCES patients(id),
  FOREIGN KEY (patient_medicine_id) REFERENCES patient_medicines(id),
  FOREIGN KEY (changed_by_user_id) REFERENCES users(id)
);

CREATE INDEX idx_medication_status_history_patient ON medication_status_history(patient_id);
CREATE INDEX idx_medication_status_history_medicine ON medication_status_history(patient_medicine_id);

3. Enhanced PatientMedicine table

-- Add status tracking to existing patient_medicines table
ALTER TABLE patient_medicines ADD COLUMN IF NOT EXISTS status VARCHAR(50) DEFAULT 'active';
ALTER TABLE patient_medicines ADD COLUMN IF NOT EXISTS discontinuation_reason TEXT;
ALTER TABLE patient_medicines ADD COLUMN IF NOT EXISTS discontinuation_date DATE;
ALTER TABLE patient_medicines ADD COLUMN IF NOT EXISTS linked_problems JSONB DEFAULT '[]';
ALTER TABLE patient_medicines ADD COLUMN IF NOT EXISTS adherence_rate INTEGER;
ALTER TABLE patient_medicines ADD COLUMN IF NOT EXISTS side_effects JSONB DEFAULT '[]';
ALTER TABLE patient_medicines ADD COLUMN IF NOT EXISTS notes TEXT;

Implementation Details

Controller Extensions

Add to ClinicPatientsLhrController

class ClinicPatientsLhrController < ApiController
  before_action :policy_check
  before_action :set_clinic_patient, except: [:show, :problems, :medications_timeline, :clinical_assessments, :vitals_trends]

  # ... existing methods ...

  # Problem management
  def create_problem
    result = ClinicPatients::CreatePatientProblem.call(problem_params.merge(
      clinic_id: params[:clinic_id],
      clinic_patient_id: params[:clinic_patient_id],
      current_user: current_user
    ))
    
    if result.success?
      render json: { success: true, problem: result.problem }
    else
      render_result(result)
    end
  end

  def update_problem
    result = ClinicPatients::UpdatePatientProblem.call(problem_params.merge(
      clinic_id: params[:clinic_id],
      clinic_patient_id: params[:clinic_patient_id],
      problem_id: params[:problem_id],
      current_user: current_user
    ))
    
    if result.success?
      render json: { success: true, problem: result.problem }
    else
      render_result(result)
    end
  end

  def resolve_problem
    result = ClinicPatients::ResolvePatientProblem.call(
      clinic_id: params[:clinic_id],
      clinic_patient_id: params[:clinic_patient_id],
      problem_id: params[:problem_id],
      resolution_notes: params[:resolution_notes],
      resolution_date: params[:resolution_date],
      current_user: current_user
    )
    
    if result.success?
      render json: { success: true, problem: result.problem }
    else
      render_result(result)
    end
  end

  def delete_problem
    result = ClinicPatients::DeletePatientProblem.call(
      clinic_id: params[:clinic_id],
      clinic_patient_id: params[:clinic_patient_id],
      problem_id: params[:problem_id],
      current_user: current_user
    )
    
    if result.success?
      render json: { success: true, message: 'Problem deleted successfully' }
    else
      render_result(result)
    end
  end

  def link_medications
    result = ClinicPatients::LinkProblemMedications.call(
      clinic_id: params[:clinic_id],
      clinic_patient_id: params[:clinic_patient_id],
      problem_id: params[:problem_id],
      medication_ids: params[:medication_ids],
      current_user: current_user
    )
    
    if result.success?
      render json: { success: true, problem: result.problem }
    else
      render_result(result)
    end
  end

  # Medication management
  def show_medication
    result = ClinicPatients::GetMedicationDetails.call(
      clinic_id: params[:clinic_id],
      clinic_patient_id: params[:clinic_patient_id],
      medication_id: params[:medication_id],
      current_user: current_user
    )
    
    if result.success?
      render json: { medication: result.medication }
    else
      render_result(result)
    end
  end

  def update_medication_status
    result = ClinicPatients::UpdateMedicationStatus.call(
      clinic_id: params[:clinic_id],
      clinic_patient_id: params[:clinic_patient_id],
      medication_id: params[:medication_id],
      status: params[:status],
      reason: params[:reason],
      effective_date: params[:effective_date],
      notes: params[:notes],
      current_user: current_user
    )
    
    if result.success?
      render json: { success: true, medication: result.medication }
    else
      render_result(result)
    end
  end

  # Encounter management
  def show_encounter
    result = ClinicPatients::GetEncounterDetails.call(
      clinic_id: params[:clinic_id],
      clinic_patient_id: params[:clinic_patient_id],
      encounter_id: params[:encounter_id],
      current_user: current_user
    )
    
    if result.success?
      render json: { encounter: result.encounter }
    else
      render_result(result)
    end
  end

  def schedule_follow_up
    result = ClinicPatients::ScheduleFollowUpAppointment.call(
      clinic_id: params[:clinic_id],
      clinic_patient_id: params[:clinic_patient_id],
      encounter_id: params[:encounter_id],
      follow_up_date: params[:follow_up_date],
      appointment_type: params[:appointment_type],
      notes: params[:notes],
      doctor_id: params[:doctor_id],
      current_user: current_user
    )
    
    if result.success?
      render json: { success: true, appointment: result.appointment }
    else
      render_result(result)
    end
  end

  private

  def set_clinic_patient
    @clinic_patient = ClinicPatient.find_by!(
      clinic_id: params[:clinic_id],
      id: params[:clinic_patient_id]
    )
  rescue ActiveRecord::RecordNotFound
    render json: { error: 'Patient not found in clinic' }, status: :not_found
  end

  def problem_params
    params.permit(:problem_name, :problem_code, :problem_category, :severity, 
                  :status, :onset_date, :clinical_notes, linked_medication_ids: [])
  end
end

Routes Configuration

Add to parts/clinic/routes.rb

# Add inside the clinic_patients member block:
member do
  # ... existing routes ...
  
  # LHR endpoints (existing)
  get :longitudinal_health_record, to: 'clinic_patients_lhr#show'
  get :problems, to: 'clinic_patients_lhr#problems'
  get :medications_timeline, to: 'clinic_patients_lhr#medications_timeline'
  get :clinical_assessments, to: 'clinic_patients_lhr#clinical_assessments'
  get :vitals_trends, to: 'clinic_patients_lhr#vitals_trends'
  
  # New LHR CRUD endpoints
  # Problem management
  post 'problems', to: 'clinic_patients_lhr#create_problem'
  put 'problems/:problem_id', to: 'clinic_patients_lhr#update_problem'
  put 'problems/:problem_id/resolve', to: 'clinic_patients_lhr#resolve_problem'
  delete 'problems/:problem_id', to: 'clinic_patients_lhr#delete_problem'
  put 'problems/:problem_id/medications', to: 'clinic_patients_lhr#link_medications'
  
  # Medication management
  get 'medications/:medication_id', to: 'clinic_patients_lhr#show_medication'
  put 'medications/:medication_id/status', to: 'clinic_patients_lhr#update_medication_status'
  
  # Encounter management
  get 'encounters/:encounter_id', to: 'clinic_patients_lhr#show_encounter'
  post 'encounters/:encounter_id/follow_up', to: 'clinic_patients_lhr#schedule_follow_up'
end

Interactor Implementation Templates

Example: CreatePatientProblem Interactor

# parts/clinic/interactors/clinic_patients/create_patient_problem.rb
module ClinicPatients
  class CreatePatientProblem < BaseInteractor
    def call
      validate_access
      validate_problem_data
      create_patient_problem
      link_medications_if_provided
      build_response
    end

    private

    def validate_access
      context.clinic_patient = ClinicPatient.find_by!(
        clinic_id: context.clinic_id,
        id: context.clinic_patient_id
      )
      context.patient = context.clinic_patient.patient
      
      # Check authorization using existing policy
      fail_with_error!('Unauthorized access') unless authorized_to_manage_patient?
    end

    def validate_problem_data
      context.fail!(message: 'Problem name is required') if context.problem_name.blank?
      context.fail!(message: 'Invalid severity') unless valid_severity?
      context.fail!(message: 'Invalid status') unless valid_status?
    end

    def create_patient_problem
      context.patient_problem = PatientProblem.create!(
        clinic_id: context.clinic_id,
        patient_id: context.patient.id,
        doctor_id: current_user_doctor_id,
        problem_name: context.problem_name,
        problem_code: context.problem_code,
        problem_category: context.problem_category || 'General',
        severity: context.severity || 'medium',
        status: context.status || 'active',
        onset_date: context.onset_date || Date.current,
        clinical_notes: context.clinical_notes,
        linked_medications: context.linked_medication_ids || []
      )
    end

    def link_medications_if_provided
      return unless context.linked_medication_ids.present?
      
      # Update patient medicines to link back to this problem
      PatientMedicine.where(
        id: context.linked_medication_ids,
        patient_id: context.patient.id
      ).find_each do |medicine|
        linked_problems = medicine.linked_problems || []
        linked_problems << context.patient_problem.id.to_s
        medicine.update!(linked_problems: linked_problems.uniq)
      end
    end

    def build_response
      context.problem = {
        id: "prob-#{context.patient_problem.id}",
        problem_name: context.patient_problem.problem_name,
        problem_code: context.patient_problem.problem_code,
        category: context.patient_problem.problem_category,
        severity: context.patient_problem.severity,
        status: context.patient_problem.status,
        onset_date: context.patient_problem.onset_date&.strftime('%Y-%m-%d'),
        duration_days: duration_since_onset,
        is_chronic: context.patient_problem.status == 'chronic',
        clinical_notes: context.patient_problem.clinical_notes,
        identified_by: current_user_name,
        last_updated: context.patient_problem.updated_at.iso8601,
        linked_medications: context.linked_medication_ids || []
      }
    end

    def valid_severity?
      %w[low medium high critical].include?(context.severity)
    end

    def valid_status?
      %w[active chronic resolved].include?(context.status)
    end

    def duration_since_onset
      return 0 unless context.patient_problem.onset_date
      (Date.current - context.patient_problem.onset_date).to_i
    end

    def current_user_doctor_id
      # Get doctor_id from current_user or clinic_doctor association
      context.current_user.clinic_doctor&.doctor_id || context.current_user.id
    end

    def current_user_name
      doctor = context.current_user.clinic_doctor&.doctor || context.current_user
      "Dr. #{doctor.first_name} #{doctor.last_name}".strip
    end

    def authorized_to_manage_patient?
      # Use existing authorization patterns
      clinic_ids = context.current_user.clinic_ids || [context.current_user.clinic_id]
      clinic_ids.include?(context.clinic_id.to_i)
    end
  end
end

Implementation Priority

Phase 1: Core Problem Management (Days 1-2)

  1. CreatePatientProblem interactor and endpoint
  2. UpdatePatientProblem interactor and endpoint
  3. DeletePatientProblem interactor and endpoint
  4. Database migrations for PatientProblem enhancements

Phase 2: Problem Resolution & Linking (Day 3)

  1. ResolvePatientProblem interactor and endpoint
  2. LinkProblemMedications interactor and endpoint
  3. Database migrations for medication linking

Phase 3: Medication Management (Day 4)

  1. GetMedicationDetails interactor and endpoint
  2. UpdateMedicationStatus interactor and endpoint
  3. Database migrations for medication status history

Phase 4: Encounter Management (Day 5)

  1. GetEncounterDetails interactor and endpoint
  2. ScheduleFollowUpAppointment interactor and endpoint
  3. Integration with existing appointment system

Testing Requirements

Unit Tests for Each Interactor

  • Valid input scenarios
  • Invalid input validation
  • Authorization checks
  • Database constraint violations
  • Edge cases (missing data, invalid IDs)

Integration Tests for Controller Endpoints

  • Full request/response cycle
  • Authentication requirements
  • Authorization policies
  • Error handling and status codes

Sample Test Structure

# spec/interactors/clinic_patients/create_patient_problem_spec.rb
RSpec.describe ClinicPatients::CreatePatientProblem do
  let(:clinic) { create(:clinic) }
  let(:patient) { create(:patient) }
  let(:clinic_patient) { create(:clinic_patient, clinic: clinic, patient: patient) }
  let(:current_user) { create(:user, clinic: clinic) }
  
  describe '#call' do
    context 'with valid parameters' do
      let(:params) do
        {
          clinic_id: clinic.id,
          clinic_patient_id: clinic_patient.id,
          problem_name: 'Type 2 Diabetes',
          severity: 'medium',
          status: 'chronic',
          current_user: current_user
        }
      end
      
      it 'creates a patient problem successfully' do
        result = described_class.call(params)
        
        expect(result.success?).to be true
        expect(result.problem[:problem_name]).to eq('Type 2 Diabetes')
        expect(PatientProblem.count).to eq(1)
      end
    end
    
    context 'with invalid parameters' do
      # Test various validation scenarios
    end
    
    context 'without authorization' do
      # Test unauthorized access scenarios  
    end
  end
end

Error Handling Strategy

Standard Error Responses

{
  "success": false,
  "error": "Problem name is required",
  "error_code": "VALIDATION_ERROR"
}

HTTP Status Codes

  • 200 - Success
  • 201 - Created
  • 400 - Bad Request (validation errors)
  • 401 - Unauthorized
  • 403 - Forbidden (authorization)
  • 404 - Not Found
  • 422 - Unprocessable Entity (business logic errors)
  • 500 - Internal Server Error

This implementation plan provides a comprehensive foundation for building the interactive LHR APIs while maintaining consistency with the existing codebase architecture and patterns.

On this page