Kulcare Docs
DeveloperCare plansCare planArchive

Longitudinal Health Record Architecture

Executive Summary

This document outlines the comprehensive architecture for transforming the current encounter-focused Careplan system into a full-featured Longitudinal Health Record (LHR) system. The design maintains backward compatibility while introducing patient-level health tracking, problem management, medication continuity, and FHIR-compliant ABDM integration.

Architecture Overview

Key Design Decisions

  1. Terminology: Rename Careplan to Encounter for industry standard alignment
  2. Data Preservation: Maintain all existing data with backward compatibility
  3. Medication Strategy: Enhance existing careplan_medicines with additional fields for continuity tracking
  4. Health Scoring: Comprehensive Wellness Score (0-100 scale)
  5. Clinical Coding: Use clinical_codes lookup table (deferred implementation)
  6. Problem Management: Use existing careplan_complaints instead of new patient_problems table
  7. Summary Field: Use existing 'notes' field instead of adding encounter_summary
  8. Access Control: Configurable at clinic level with patient consent for referrals
  9. AI Integration: Mock implementation initially
  10. ABDM Compliance: FHIR-compliant data structures

System Components

┌─────────────────────────────────────────────────────────────┐
│                      Frontend (React)                        │
├─────────────────────────────────────────────────────────────┤
│                    API Layer (Rails)                         │
│  ┌──────────────────────────────────────────────────────┐  │
│  │  Backward Compatible APIs    │    New LHR APIs        │  │
│  └──────────────────────────────────────────────────────┘  │
├─────────────────────────────────────────────────────────────┤
│                    Service Layer                             │
│  ┌────────────┬─────────────┬──────────────┬────────────┐  │
│  │ Health     │ Medication  │ Problem      │ AI Scribe  │  │
│  │ Score      │ Continuity  │ Management   │ (Mock)     │  │
│  └────────────┴─────────────┴──────────────┴────────────┘  │
├─────────────────────────────────────────────────────────────┤
│                    Model Layer                               │
│  ┌────────────┬─────────────┬──────────────┬────────────┐  │
│  │ Encounter  │ Patient     │ Medication   │ Clinical   │  │
│  │ (Careplan) │ Problems    │ Prescriptions│ Codes      │  │
│  └────────────┴─────────────┴──────────────┴────────────┘  │
├─────────────────────────────────────────────────────────────┤
│                  Database (PostgreSQL)                       │
└─────────────────────────────────────────────────────────────┘

Database Design

1. Core Migration: Rename Careplan to Encounter

# db/migrate/[timestamp]_rename_careplans_to_encounters.rb
class RenameCareplansToEncounters < ActiveRecord::Migration[7.0]
  def change
    # Rename main table
    rename_table :careplans, :encounters
    
    # Rename foreign keys in related tables
    rename_column :careplan_complaints, :careplan_id, :encounter_id
    rename_table :careplan_complaints, :encounter_complaints
    
    rename_column :careplan_medicines, :careplan_id, :encounter_id
    rename_table :careplan_medicines, :encounter_medicines
    
    rename_column :careplan_procedures, :careplan_id, :encounter_id
    rename_table :careplan_procedures, :encounter_procedures
    
    rename_column :careplan_tests, :careplan_id, :encounter_id
    rename_table :careplan_tests, :encounter_tests
    
    rename_column :careplan_test_lists, :careplan_id, :encounter_id
    rename_table :careplan_test_lists, :encounter_test_lists
    
    rename_column :careplan_diets, :careplan_id, :encounter_id
    rename_table :careplan_diets, :encounter_diets
    
    rename_column :careplan_advices, :careplan_id, :encounter_id
    rename_table :careplan_advices, :encounter_advices
    
    # Add new columns to encounters
    add_column :encounters, :encounter_type, :string # routine/emergency/followup/telemedicine
    add_column :encounters, :health_score_snapshot, :float
    add_column :encounters, :reconciliation_completed, :boolean, default: false
    add_column :encounters, :fhir_resource_id, :string
    add_index :encounters, :fhir_resource_id
    
    # Note: Use existing 'notes' field instead of encounter_summary
    
    # Update polymorphic references
    execute <<-SQL
      UPDATE documents 
      SET documentable_type = 'Encounter' 
      WHERE documentable_type = 'Careplan';
      
      UPDATE activities 
      SET trackable_type = 'Encounter' 
      WHERE trackable_type = 'Careplan';
    SQL
  end
end

2. Enhanced Encounter Complaints (Using Existing Structure)

# db/migrate/[timestamp]_enhance_encounter_complaints.rb
class EnhanceEncounterComplaints < ActiveRecord::Migration[7.0]
  def change
    # Enhance existing encounter_complaints table for problem management
    add_column :encounter_complaints, :status, :string, default: 'active' # active/resolved/chronic/in_remission
    add_column :encounter_complaints, :severity, :string # low/medium/high/critical
    add_column :encounter_complaints, :onset_date, :date
    add_column :encounter_complaints, :resolved_date, :date
    add_column :encounter_complaints, :clinical_notes, :jsonb
    add_column :encounter_complaints, :affects_health_score, :boolean, default: true
    
    # Add indexes for performance
    add_index :encounter_complaints, :status
    add_index :encounter_complaints, :severity
    
    # Note: Use existing encounter_complaints instead of creating patient_problems
    # This maintains current structure while adding longitudinal tracking capabilities
  end
end

3. Enhanced Encounter Medicines (Extending Existing Structure)

# db/migrate/[timestamp]_enhance_encounter_medicines.rb
class EnhanceEncounterMedicines < ActiveRecord::Migration[7.0]
  def change
    # Enhance existing encounter_medicines table for prescription continuity
    add_column :encounter_medicines, :generic_name, :string
    add_column :encounter_medicines, :route, :string
    add_column :encounter_medicines, :prescribed_date, :date
    add_column :encounter_medicines, :start_date, :date
    add_column :encounter_medicines, :end_date, :date
    add_column :encounter_medicines, :status, :string, default: 'active' # active/completed/discontinued/on_hold
    add_column :encounter_medicines, :prescription_type, :string # acute/chronic/prn/prophylactic
    add_column :encounter_medicines, :discontinuation_reason, :string
    
    # Reconciliation tracking
    add_column :encounter_medicines, :verified_in_reconciliation, :boolean, default: false
    add_column :encounter_medicines, :last_reconciliation_date, :date
    
    # Clinical decision support
    add_column :encounter_medicines, :drug_interactions, :jsonb
    add_column :encounter_medicines, :allergy_alerts, :jsonb
    
    # Pharmacy integration
    add_column :encounter_medicines, :pharmacy_order_id, :string
    add_column :encounter_medicines, :pharmacy_status, :string
    
    # Parent-child relationship for refills/modifications
    add_column :encounter_medicines, :parent_medicine_id, :integer
    add_index :encounter_medicines, :parent_medicine_id
    
    # Add indexes for performance
    add_index :encounter_medicines, :status
    add_index :encounter_medicines, :prescription_type
    
    # Note: Keeping existing encounter_medicines structure and extending it
    # This maintains backward compatibility while adding new functionality
  end
end

4. Medication Reconciliation Tracking

# db/migrate/[timestamp]_create_medication_reconciliations.rb
class CreateMedicationReconciliations < ActiveRecord::Migration[7.0]
  def change
    create_table :medication_reconciliations do |t|
      t.references :encounter, null: false, foreign_key: true
      t.references :medication_prescription, foreign_key: true
      t.string :action, null: false # continued/modified/discontinued/new
      t.text :reason
      t.jsonb :changes # tracks what was modified
      t.references :performed_by_user, foreign_key: { to_table: :users }
      t.timestamps
    end
    
    add_index :medication_reconciliations, [:encounter_id, :medication_prescription_id], 
              name: 'idx_med_recon_encounter_prescription'
  end
end

5. Health Score System

# db/migrate/[timestamp]_create_patient_health_scores.rb
class CreatePatientHealthScores < ActiveRecord::Migration[7.0]
  def change
    create_table :patient_health_scores do |t|
      t.references :patient, null: false, foreign_key: true
      t.references :clinic_patient, foreign_key: true
      
      # Overall and component scores (0-100 scale)
      t.float :overall_score, null: false
      t.float :vital_score
      t.float :problem_score
      t.float :medication_score
      t.float :preventive_score
      
      # Detailed breakdown
      t.jsonb :score_components
      t.jsonb :risk_factors
      
      # Tracking
      t.date :calculated_date, null: false
      t.references :encounter, foreign_key: true
      
      t.timestamps
    end
    
    add_index :patient_health_scores, [:patient_id, :calculated_date]
  end
end

6. Longitudinal Health Summary

# db/migrate/[timestamp]_create_patient_health_summaries.rb
class CreatePatientHealthSummaries < ActiveRecord::Migration[7.0]
  def change
    create_table :patient_health_summaries do |t|
      t.references :patient, null: false, foreign_key: true
      t.references :clinic_patient, foreign_key: true
      
      # Current state
      t.float :current_health_score
      t.jsonb :vital_trends
      t.jsonb :active_problems_summary
      t.jsonb :current_medications_summary
      
      # Statistics
      t.integer :total_encounters_count
      t.date :last_encounter_date
      t.date :next_appointment_date
      
      # Key metrics
      t.jsonb :key_metrics # BP trends, weight trends, etc
      
      # ABDM Integration
      t.jsonb :fhir_bundle
      t.datetime :last_abdm_sync
      
      t.timestamps
    end
    
    add_index :patient_health_summaries, :patient_id, unique: true
  end
end

7. Clinical Coding Support (DEFERRED)

# NOTE: Clinical codes implementation is deferred to a later phase
# The clinical_codes lookup table will be implemented when needed
# This will replace direct storage of ICD-10, ICD-11, SNOMED, RxNorm codes

# Future implementation will include:
# - clinical_codes table with code_system, code, display_name
# - Lookup functionality for encounter_complaints and encounter_medicines  
# - Standardized medical coding across the system
# - Integration with international coding standards

# For now, medical codes will be stored as simple text fields in:
# - encounter_complaints (existing name field)
# - encounter_medicines (existing medicine_name field)

8. AI Scribe Mock System (OPTIONAL)

# db/migrate/[timestamp]_create_ai_transcriptions.rb
# NOTE: AI Scribe is optional feature - can be implemented later
class CreateAiTranscriptions < ActiveRecord::Migration[7.0]
  def change
    create_table :ai_transcriptions do |t|
      # Reference to encounters table (renamed from careplans)
      t.references :encounter, null: false, foreign_key: { to_table: :encounters }
      
      # Transcription data
      t.text :raw_transcript
      t.jsonb :extracted_entities
      t.jsonb :suggestions
      
      # Metadata
      t.string :confidence_level
      t.string :processing_status
      t.float :processing_duration
      
      # Acceptance tracking
      t.boolean :accepted_by_doctor, default: false
      t.jsonb :acceptance_details
      
      t.timestamps
    end
  end
end
# db/migrate/[timestamp]_add_longitudinal_access_settings.rb
# NOTE: Access control features can be implemented in later phases
class AddLongitudinalAccessSettings < ActiveRecord::Migration[7.0]
  def change
    # Clinic-level settings (if clinic_settings table exists)
    add_column :clinic_settings, :longitudinal_record_access, :string, default: 'all_doctors'
    # Options: all_doctors, treating_doctor_only, department_based
    
    add_column :clinic_settings, :require_patient_consent_for_referral, :boolean, default: true
    
    # Patient consent records - for managing cross-doctor access
    create_table :patient_consent_records do |t|
      t.references :patient, null: false, foreign_key: true
      t.references :referred_doctor, foreign_key: { to_table: :doctors }
      t.references :referring_doctor, foreign_key: { to_table: :doctors }
      t.references :clinic, foreign_key: true
      
      # Consent details
      t.string :consent_type # view_only/full_access/emergency_only
      t.string :consent_scope # all_records/specific_encounters/date_range
      t.jsonb :scope_details
      
      # Validity period
      t.date :valid_from
      t.date :valid_until
      t.boolean :revoked, default: false
      t.datetime :revoked_at
      
      t.timestamps
    end
    
    # Indexes for efficient querying
    add_index :patient_consent_records, [:patient_id, :referred_doctor_id]
    add_index :patient_consent_records, :valid_until
  end
end

Implementation Challenges & Solutions

Challenge 1: Final Careplan Edit Restriction

Problem: Currently, when is_final = true, careplans cannot be edited, preventing longitudinal tracking of problems and medications.

Solution: Implement post-finalization editing for longitudinal data while preserving core careplan immutability.

# app/models/encounter.rb (renamed from careplan.rb)
class Encounter < ApplicationRecord
  # ... existing associations and methods
  
  # Allow editing of longitudinal fields even after finalization
  def can_edit_longitudinal_fields?
    true # Always allow editing longitudinal tracking fields
  end
  
  def can_edit_core_fields?
    !is_final? # Existing behavior for core careplan fields
  end
  
  # Separate validation for core vs longitudinal fields
  def validate_core_changes
    if is_final? && core_fields_changed?
      errors.add(:base, "Cannot modify finalized encounter core data")
    end
  end
  
  private
  
  def core_fields_changed?
    changed_attributes.keys.any? do |attr|
      %w[notes diagnosis advice diet observations].include?(attr)
    end
  end
end

Challenge 2: End Date Calculation for Medicines

Problem: encounter_medicines has duration_unit and duration_frequency but no end_date field for tracking.

Solution: Add end_date calculation and automatic population.

# Enhanced encounter_medicines model
class EncounterMedicine < ApplicationRecord
  # ... existing associations
  
  # Callbacks
  before_save :calculate_end_date
  before_save :set_prescribed_date
  
  private
  
  def calculate_end_date
    return unless start_date.present? && duration_unit.present? && duration_frequency.present?
    
    self.end_date = case duration_frequency.upcase
                   when 'DAYS'
                     start_date + duration_unit.to_i.days
                   when 'WEEKS'
                     start_date + duration_unit.to_i.weeks
                   when 'MONTHS'
                     start_date + duration_unit.to_i.months
                   when 'YEARS'
                     start_date + duration_unit.to_i.years
                   else
                     start_date + duration_unit.to_i.days # Default to days
                   end
  end
  
  def set_prescribed_date
    self.prescribed_date ||= Date.current
    self.start_date ||= prescribed_date
  end
end

Enhanced CareplansController Strategy

Extend Existing Controller

Add new actions to the existing CareplansController for longitudinal features:

# parts/doctor/controllers/careplans_controller.rb (Enhanced)
class CareplansController < ApiController
  before_action :policy_check
  before_action :set_careplan, only: [:show, :update_longitudinal, :update_complaints, :update_medicines]

  # Existing methods remain unchanged
  def create
    result = Careplans::Create.call(params)
    render_result(result, result.success? ? CareplanSerializer : nil)
  end

  def index
    result = Careplans::Index.call(params)
    render_result(result, result.success? ? CareplanSerializer : nil)
  end

  # NEW: Update longitudinal fields even after finalization
  def update_longitudinal
    result = Careplans::UpdateLongitudinal.call(params.merge(careplan: @careplan))
    render_result(result, CareplanSerializer)
  end

  # NEW: Update complaints with longitudinal tracking
  def update_complaints
    result = Careplans::UpdateComplaints.call(params.merge(careplan: @careplan))
    render_result(result, CareplanComplaintSerializer)
  end

  # NEW: Update medicines with status tracking
  def update_medicines
    result = Careplans::UpdateMedicines.call(params.merge(careplan: @careplan))
    render_result(result, CareplanMedicineSerializer)
  end

  private

  def set_careplan
    @careplan = Careplan.find(params[:id])
  end

  def policy_check
    authorize params, policy_class: CareplansControllerPolicy
  end
end

Enhanced Interactor Design

Modified Existing Create Interactor

# parts/doctor/interactors/careplans/create.rb (Enhanced)
module Careplans
  class Create < BaseOrganizer
    # Keep existing organize structure, add new interactors
    organize Careplans::SaveCareplan,
             Careplans::SetEncounterFields,       # NEW: Set encounter_type, health_score_snapshot
             Careplans::SaveComplaints,
             Careplans::EnhanceComplaints,        # NEW: Add longitudinal fields to complaints
             Careplans::SaveMedicines,
             Careplans::EnhanceMedicines,         # NEW: Add longitudinal fields to medicines
             Careplans::SaveTests,
             Careplans::SaveProcedures,
             Careplans::SaveDiets,
             Careplans::SaveAdvices,
             Careplans::GenerateCareplan,
             Careplans::GeneratePrescription,
             Careplans::GenerateMedicines,
             Careplans::GeneratePatientCases,
             Careplans::SendToLab,
             Careplans::SendToPharmacy,
             Careplans::SendToAbdm,
             Careplans::SavePatientHistory
             # Note: Health score calculation happens after finalization
  end
end

New Interactors for Longitudinal Updates

# parts/doctor/interactors/careplans/update_longitudinal.rb
module Careplans
  class UpdateLongitudinal < BaseInteractor
    delegate :careplan, to: :context
    
    def call
      # Allow updates to longitudinal fields even if careplan is final
      update_longitudinal_fields
      calculate_health_score if should_recalculate_health_score?
    end
    
    private
    
    def update_longitudinal_fields
      allowed_params = context.to_h.slice(
        :encounter_type, :health_score_snapshot, :fhir_resource_id, 
        :reconciliation_completed
      )
      
      careplan.update!(allowed_params) if allowed_params.any?
      context.careplan = careplan
    end
    
    def should_recalculate_health_score?
      context.recalculate_health_score == true
    end
    
    def calculate_health_score
      # Calculate and update health score
      if careplan.patient_id.present?
        score = LongitudinalHealthService.new(careplan.patient_id).calculate_health_score
        careplan.update!(health_score_snapshot: score.overall_score)
      end
    end
  end
end

# parts/doctor/interactors/careplans/update_complaints.rb
module Careplans
  class UpdateComplaints < BaseInteractor
    delegate :careplan, to: :context
    
    def call
      complaints_params = context.complaints || []
      
      complaints_params.each do |complaint_data|
        if complaint_data[:id].present?
          update_existing_complaint(complaint_data)
        else
          create_new_complaint(complaint_data)
        end
      end
      
      trigger_health_score_recalculation if any_health_affecting_changes?
    end
    
    private
    
    def update_existing_complaint(complaint_data)
      complaint = careplan.careplan_complaints.find(complaint_data[:id])
      
      # Allow longitudinal field updates even if careplan is final
      longitudinal_fields = complaint_data.slice(
        :status, :severity, :onset_date, :resolved_date, 
        :clinical_notes, :affects_health_score
      )
      
      complaint.update!(longitudinal_fields) if longitudinal_fields.any?
    end
    
    def create_new_complaint(complaint_data)
      # Set defaults for longitudinal tracking
      enhanced_data = complaint_data.merge(
        status: complaint_data[:status] || 'active',
        onset_date: complaint_data[:onset_date] || Date.current,
        affects_health_score: complaint_data[:affects_health_score] != false
      )
      
      careplan.careplan_complaints.create!(enhanced_data)
    end
    
    def any_health_affecting_changes?
      context.complaints&.any? { |c| c[:affects_health_score] != false }
    end
    
    def trigger_health_score_recalculation
      CalculateHealthScoreJob.perform_later(careplan.patient_id, careplan.id)
    end
  end
end

# parts/doctor/interactors/careplans/update_medicines.rb
module Careplans
  class UpdateMedicines < BaseInteractor
    delegate :careplan, to: :context
    
    def call
      medicines_params = context.medicines || []
      
      medicines_params.each do |medicine_data|
        if medicine_data[:id].present?
          update_existing_medicine(medicine_data)
        else
          create_new_medicine(medicine_data)
        end
      end
      
      trigger_health_score_recalculation
    end
    
    private
    
    def update_existing_medicine(medicine_data)
      medicine = careplan.careplan_medicines.find(medicine_data[:id])
      
      # Allow longitudinal field updates even if careplan is final
      longitudinal_fields = medicine_data.slice(
        :status, :prescription_type, :discontinuation_reason, 
        :verified_in_reconciliation, :last_reconciliation_date, 
        :drug_interactions, :allergy_alerts, :pharmacy_order_id, 
        :pharmacy_status, :parent_medicine_id
      )
      
      medicine.update!(longitudinal_fields) if longitudinal_fields.any?
    end
    
    def create_new_medicine(medicine_data)
      # Set defaults for longitudinal tracking
      enhanced_data = medicine_data.merge(
        status: medicine_data[:status] || 'active',
        prescription_type: medicine_data[:prescription_type] || 'acute',
        prescribed_date: medicine_data[:prescribed_date] || Date.current,
        start_date: medicine_data[:start_date] || Date.current,
        verified_in_reconciliation: false
      )
      
      careplan.careplan_medicines.create!(enhanced_data)
    end
    
    def trigger_health_score_recalculation
      CalculateHealthScoreJob.perform_later(careplan.patient_id, careplan.id)
    end
  end
end

# parts/doctor/interactors/careplans/set_encounter_fields.rb
module Careplans
  class SetEncounterFields < BaseInteractor
    delegate :careplan, to: :context
    
    def call
      return unless careplan.present?
      
      encounter_type = determine_encounter_type
      
      careplan.update!(
        encounter_type: encounter_type,
        reconciliation_completed: false
      )
      
      # health_score_snapshot will be calculated after finalization
    end
    
    private
    
    def determine_encounter_type
      # Determine type based on context params
      if context.encounter_type.present?
        context.encounter_type
      elsif context.emergency == true
        'emergency'
      elsif context.is_followup == true
        'followup'
      elsif context.telemedicine == true
        'telemedicine'
      else
        'routine'
      end
    end
  end
end

# parts/doctor/interactors/careplans/enhance_complaints.rb
module Careplans
  class EnhanceComplaints < BaseInteractor
    delegate :careplan, to: :context
    
    def call
      return unless careplan&.careplan_complaints&.any?
      
      careplan.careplan_complaints.each do |complaint|
        # Set default longitudinal tracking values for new complaints
        next if complaint.status.present? # Skip if already enhanced
        
        complaint.update!(
          status: 'active',
          onset_date: careplan.created_at.to_date,
          affects_health_score: true
        )
      end
    end
  end
end

# parts/doctor/interactors/careplans/enhance_medicines.rb
module Careplans
  class EnhanceMedicines < BaseInteractor
    delegate :careplan, to: :context
    
    def call
      return unless careplan&.careplan_medicines&.any?
      
      careplan.careplan_medicines.each do |medicine|
        # Set default longitudinal tracking values for new medicines
        next if medicine.status.present? # Skip if already enhanced
        
        medicine.update!(
          status: 'active',
          prescription_type: 'acute',
          prescribed_date: careplan.created_at.to_date,
          start_date: careplan.created_at.to_date,
          verified_in_reconciliation: false
        )
        
        # end_date will be calculated automatically via before_save callback
      end
    end
  end
end

Routes Configuration

# Add to parts/doctor/routes.rb
Rails.application.routes.draw do
  scope :doctor do
    resources :careplans do
      member do
        patch :update_longitudinal    # PATCH /careplans/:id/update_longitudinal
        patch :update_complaints     # PATCH /careplans/:id/update_complaints
        patch :update_medicines      # PATCH /careplans/:id/update_medicines
      end
    end
  end
end

Model Layer

Core Models

# app/models/encounter.rb (renamed from careplan.rb)
class Encounter < ApplicationRecord
  include PublicActivity::Common
  include AbdmCareContextFunctions
  include AbdmFhirFunctions
  include PatientHistoryActivity
  
  # Associations
  belongs_to :doctor
  belongs_to :patient, optional: true
  belongs_to :clinic_patient, optional: true
  belongs_to :clinic, optional: true
  belongs_to :event, polymorphic: true, optional: true
  
  # Renamed associations
  has_many :encounter_complaints, dependent: :destroy
  has_many :encounter_medicines, dependent: :destroy
  has_many :encounter_procedures, dependent: :destroy
  has_many :encounter_test_lists, dependent: :destroy
  has_many :encounter_tests, through: :encounter_test_lists
  has_many :encounter_diets, dependent: :destroy
  has_many :encounter_advices, dependent: :destroy
  
  # New associations
  has_many :medication_prescriptions, dependent: :destroy
  has_many :medication_reconciliations, dependent: :destroy
  has_many :encounter_problems, dependent: :destroy
  has_many :patient_problems, through: :encounter_problems
  has_one :ai_transcription, dependent: :destroy
  has_one :patient_health_score, dependent: :nullify
  
  # Existing associations (unchanged)
  has_many :pharmacy_orders, as: :event, dependent: :destroy
  has_many :lab_orders, as: :event, dependent: :destroy
  has_many :documents
  has_many :shared_prescriptions
  
  # Scopes
  scope :recent, -> { order(created_at: :desc) }
  scope :final, -> { where(is_final: true) }
  scope :with_problems, -> { joins(:patient_problems).distinct }
  scope :with_reconciliation, -> { where(reconciliation_completed: true) }
  
  # Callbacks
  after_create :update_patient_health_summary
  after_update :recalculate_health_score, if: :saved_change_to_is_final?
  
  # Backward compatibility
  alias_attribute :careplan_complaints, :encounter_complaints
  alias_attribute :careplan_medicines, :encounter_medicines
  
  # Validations
  validates :encounter_type, inclusion: { 
    in: %w[routine emergency followup telemedicine],
    allow_nil: true 
  }
  
  private
  
  def update_patient_health_summary
    return unless patient_id.present?
    UpdatePatientHealthSummaryJob.perform_later(patient_id)
  end
  
  def recalculate_health_score
    return unless is_final? && patient_id.present?
    CalculateHealthScoreJob.perform_later(patient_id, id)
  end
end

# app/models/patient_problem.rb
class PatientProblem < ApplicationRecord
  # Associations
  belongs_to :patient
  belongs_to :clinic_patient, optional: true
  belongs_to :created_by_encounter, class_name: 'Encounter', optional: true
  belongs_to :resolved_by_encounter, class_name: 'Encounter', optional: true
  belongs_to :created_by_doctor, class_name: 'Doctor', optional: true
  
  has_many :encounter_problems, dependent: :destroy
  has_many :encounters, through: :encounter_problems
  
  # Validations
  validates :name, presence: true
  validates :status, inclusion: { 
    in: %w[active resolved chronic in_remission] 
  }
  validates :severity, inclusion: { 
    in: %w[low medium high critical],
    allow_nil: true 
  }
  validate :must_have_medical_code
  
  # Scopes
  scope :active, -> { where(status: 'active') }
  scope :chronic, -> { where(status: 'chronic') }
  scope :resolved, -> { where(status: 'resolved') }
  scope :by_severity, -> { order(Arel.sql("CASE severity 
    WHEN 'critical' THEN 0 
    WHEN 'high' THEN 1 
    WHEN 'medium' THEN 2 
    WHEN 'low' THEN 3 
    ELSE 4 END")) }
  
  # Callbacks
  after_save :update_patient_health_summary
  
  # Methods
  def duration_days
    return nil unless onset_date.present?
    end_date = resolved_date || Date.current
    (end_date - onset_date).to_i
  end
  
  def medical_codes
    codes = []
    codes << { system: 'ICD-10', code: icd10_code } if icd10_code.present?
    codes << { system: 'ICD-11', code: icd11_code } if icd11_code.present?
    codes << { system: 'SNOMED-CT', code: snomed_code } if snomed_code.present?
    codes
  end
  
  private
  
  def must_have_medical_code
    if icd10_code.blank? && icd11_code.blank? && snomed_code.blank?
      errors.add(:base, "Must have at least one medical code (ICD-10, ICD-11, or SNOMED-CT)")
    end
  end
  
  def update_patient_health_summary
    UpdatePatientHealthSummaryJob.perform_later(patient_id)
  end
end

# app/models/medication_prescription.rb
class MedicationPrescription < ApplicationRecord
  # Associations
  belongs_to :patient
  belongs_to :clinic_patient, optional: true
  belongs_to :encounter
  belongs_to :doctor
  belongs_to :parent_prescription, 
             class_name: 'MedicationPrescription', 
             optional: true
  belongs_to :discontinued_by_encounter, 
             class_name: 'Encounter', 
             optional: true
  
  has_many :refills, 
           class_name: 'MedicationPrescription', 
           foreign_key: :parent_prescription_id,
           dependent: :nullify
  has_many :medication_reconciliations, dependent: :destroy
  
  # Validations
  validates :medication_name, presence: true
  validates :status, inclusion: { 
    in: %w[active completed discontinued on_hold] 
  }
  validates :prescription_type, inclusion: { 
    in: %w[acute chronic prn prophylactic],
    allow_nil: true 
  }
  
  # Scopes
  scope :active, -> { where(status: 'active') }
  scope :chronic, -> { where(prescription_type: 'chronic') }
  scope :requiring_reconciliation, -> { 
    active.where('last_reconciliation_date < ? OR last_reconciliation_date IS NULL', 
                 30.days.ago) 
  }
  
  # Callbacks
  before_save :calculate_end_date
  after_save :check_drug_interactions
  
  # Methods
  def days_remaining
    return nil unless end_date.present? && end_date.future?
    (end_date - Date.current).to_i
  end
  
  def duration_completed?
    return false unless end_date.present?
    Date.current >= end_date
  end
  
  def requires_refill?
    chronic? && days_remaining.present? && days_remaining <= 7
  end
  
  private
  
  def calculate_end_date
    if start_date.present? && duration_value.present? && duration_unit.present?
      self.end_date = start_date + duration_value.send(duration_unit)
    end
  end
  
  def check_drug_interactions
    CheckDrugInteractionsJob.perform_later(id) if saved_change_to_medication_name?
  end
end

Service Layer

Health Score Calculation Service

# app/services/longitudinal_health_service.rb
class LongitudinalHealthService
  def initialize(patient_id)
    @patient = Patient.find(patient_id)
    @clinic_patient = @patient.clinic_patients.last
  end
  
  def build_complete_record
    {
      demographics: patient_demographics,
      health_score: current_health_score,
      active_problems: active_problems_list,
      current_medications: current_medications_list,
      vital_trends: calculate_vital_trends,
      encounter_timeline: encounter_history,
      upcoming_preventive_care: preventive_care_reminders,
      last_updated: Time.current
    }
  end
  
  def calculate_health_score
    # Comprehensive Wellness Score (0-100)
    vital_score = calculate_vital_score
    problem_score = calculate_problem_score
    medication_score = calculate_medication_adherence_score
    preventive_score = calculate_preventive_care_score
    
    # Weighted average
    overall = (
      vital_score * 0.25 +      # 25% weight
      problem_score * 0.30 +     # 30% weight
      medication_score * 0.25 +  # 25% weight
      preventive_score * 0.20    # 20% weight
    ).round(2)
    
    # Save the score
    PatientHealthScore.create!(
      patient: @patient,
      clinic_patient: @clinic_patient,
      overall_score: overall,
      vital_score: vital_score,
      problem_score: problem_score,
      medication_score: medication_score,
      preventive_score: preventive_score,
      score_components: {
        vital_details: vital_score_details,
        problem_details: problem_score_details,
        medication_details: medication_score_details,
        preventive_details: preventive_score_details
      },
      calculated_date: Date.current
    )
  end
  
  private
  
  def calculate_vital_score
    recent_vitals = @patient.patient_vitals.recent.limit(10)
    return 100.0 if recent_vitals.empty?
    
    normal_count = recent_vitals.count { |v| v.within_normal_range? }
    (normal_count.to_f / recent_vitals.count * 100).round(2)
  end
  
  def calculate_problem_score
    active_problems = @patient.patient_problems.active
    base_score = 100.0
    
    active_problems.each do |problem|
      case problem.severity
      when 'critical' then base_score -= 20
      when 'high' then base_score -= 15
      when 'medium' then base_score -= 10
      when 'low' then base_score -= 5
      end
    end
    
    # Factor in chronic conditions
    chronic_count = @patient.patient_problems.chronic.count
    base_score -= (chronic_count * 5)
    
    [base_score, 0].max # Ensure non-negative
  end
  
  def calculate_medication_adherence_score
    active_prescriptions = @patient.medication_prescriptions.active
    return 100.0 if active_prescriptions.empty?
    
    # For MVP, assume 80% adherence (will be calculated from actual data later)
    base_adherence = 80.0
    
    # Deduct for medications requiring reconciliation
    requiring_recon = active_prescriptions.requiring_reconciliation.count
    base_adherence -= (requiring_recon * 5)
    
    [base_adherence, 0].max
  end
  
  def calculate_preventive_care_score
    # For MVP, return a baseline score
    # Future: Calculate based on immunizations, screenings, etc.
    75.0
  end
  
  def vital_score_details
    {
      total_checked: @patient.patient_vitals.recent.limit(10).count,
      normal_count: @patient.patient_vitals.recent.limit(10).count { |v| v.within_normal_range? },
      last_checked: @patient.patient_vitals.recent.first&.created_at
    }
  end
  
  def problem_score_details
    {
      active_count: @patient.patient_problems.active.count,
      chronic_count: @patient.patient_problems.chronic.count,
      by_severity: @patient.patient_problems.active.group(:severity).count
    }
  end
  
  def medication_score_details
    {
      active_count: @patient.medication_prescriptions.active.count,
      requiring_reconciliation: @patient.medication_prescriptions.requiring_reconciliation.count,
      chronic_medications: @patient.medication_prescriptions.chronic.count
    }
  end
  
  def preventive_score_details
    {
      status: 'baseline',
      next_screening_due: nil
    }
  end
  
  def patient_demographics
    {
      id: @patient.id,
      name: @patient.title,
      age: @patient.patient_age,
      gender: @patient.gender,
      blood_group: @patient.blood_group
    }
  end
  
  def active_problems_list
    @patient.patient_problems.active.includes(:created_by_encounter).map do |problem|
      {
        id: problem.id,
        name: problem.name,
        severity: problem.severity,
        onset_date: problem.onset_date,
        duration_days: problem.duration_days,
        medical_codes: problem.medical_codes
      }
    end
  end
  
  def current_medications_list
    @patient.medication_prescriptions.active.includes(:doctor, :encounter).map do |prescription|
      {
        id: prescription.id,
        medication_name: prescription.medication_name,
        dosage: prescription.dosage,
        frequency: prescription.frequency,
        start_date: prescription.start_date,
        days_remaining: prescription.days_remaining,
        requires_refill: prescription.requires_refill?,
        prescribed_by: prescription.doctor.name
      }
    end
  end
  
  def calculate_vital_trends(days_back = 90)
    end_date = Date.current
    start_date = end_date - days_back.days
    
    vitals = @patient.patient_vitals
                     .where(created_at: start_date..end_date)
                     .order(:created_at)
    
    {
      blood_pressure: extract_bp_trend(vitals),
      weight: extract_weight_trend(vitals),
      heart_rate: extract_heart_rate_trend(vitals)
    }
  end
  
  def encounter_history
    @patient.encounters.includes(:doctor).recent.limit(10).map do |encounter|
      {
        id: encounter.id,
        date: encounter.created_at,
        type: encounter.encounter_type,
        doctor: encounter.doctor.name,
        chief_complaint: encounter.encounter_complaints.first&.name,
        summary: encounter.encounter_summary
      }
    end
  end
  
  def preventive_care_reminders
    # Placeholder for preventive care logic
    []
  end
end

Medication Reconciliation Service

# app/services/medication_reconciliation_service.rb
class MedicationReconciliationService
  def initialize(encounter)
    @encounter = encounter
    @patient = encounter.patient
    @doctor = encounter.doctor
  end
  
  def perform_reconciliation
    current_medications = @patient.medication_prescriptions.active.includes(:doctor)
    
    reconciliation_list = current_medications.map do |medication|
      {
        prescription: serialize_prescription(medication),
        suggested_action: suggest_action(medication),
        drug_interactions: check_interactions(medication),
        requires_review: requires_clinical_review?(medication),
        last_reconciliation: medication.last_reconciliation_date
      }
    end
    
    {
      encounter_id: @encounter.id,
      patient_id: @patient.id,
      medications: reconciliation_list,
      performed_at: Time.current
    }
  end
  
  def record_reconciliation_decision(prescription_id, action, reason = nil, changes = nil)
    prescription = MedicationPrescription.find(prescription_id)
    
    ActiveRecord::Base.transaction do
      # Create reconciliation record
      reconciliation = MedicationReconciliation.create!(
        encounter: @encounter,
        medication_prescription: prescription,
        action: action,
        reason: reason,
        changes: changes,
        performed_by_user: Current.user
      )
      
      # Update prescription based on action
      case action
      when 'continued'
        prescription.update!(
          verified_in_reconciliation: true,
          last_reconciliation_date: Date.current
        )
      when 'modified'
        apply_modifications(prescription, changes)
      when 'discontinued'
        prescription.update!(
          status: 'discontinued',
          discontinuation_reason: reason,
          discontinued_by_encounter: @encounter,
          end_date: Date.current
        )
      when 'new'
        # New prescription created separately
      end
      
      reconciliation
    end
  end
  
  def complete_reconciliation
    @encounter.update!(reconciliation_completed: true)
    update_patient_medication_summary
  end
  
  private
  
  def serialize_prescription(prescription)
    {
      id: prescription.id,
      medication_name: prescription.medication_name,
      generic_name: prescription.generic_name,
      dosage: prescription.dosage,
      frequency: prescription.frequency,
      route: prescription.route,
      start_date: prescription.start_date,
      end_date: prescription.end_date,
      prescription_type: prescription.prescription_type,
      prescribed_by: prescription.doctor.name,
      days_active: (Date.current - prescription.start_date).to_i
    }
  end
  
  def suggest_action(medication)
    if medication.end_date&.past?
      'discontinue'
    elsif medication.prescription_type == 'acute' && medication.duration_completed?
      'complete'
    elsif medication.requires_refill?
      'refill'
    else
      'continue'
    end
  end
  
  def check_interactions(medication)
    # Mock implementation - would integrate with drug interaction API
    other_medications = @patient.medication_prescriptions
                                .active
                                .where.not(id: medication.id)
    
    interactions = []
    
    # Simplified interaction check
    other_medications.each do |other|
      if potential_interaction?(medication, other)
        interactions << {
          medication: other.medication_name,
          severity: 'moderate',
          description: 'Potential interaction detected'
        }
      end
    end
    
    interactions
  end
  
  def requires_clinical_review?(medication)
    # High-risk medications or those with interactions
    medication.prescription_type == 'chronic' ||
    medication.drug_interactions.present? ||
    medication.last_reconciliation_date.nil? ||
    medication.last_reconciliation_date < 60.days.ago
  end
  
  def potential_interaction?(med1, med2)
    # Mock logic - would use real drug interaction database
    false
  end
  
  def apply_modifications(prescription, changes)
    updateable_attributes = changes.slice(
      :dosage, :frequency, :route, :duration_value, :duration_unit
    )
    
    prescription.update!(
      updateable_attributes.merge(
        verified_in_reconciliation: true,
        last_reconciliation_date: Date.current
      )
    )
  end
  
  def update_patient_medication_summary
    UpdatePatientHealthSummaryJob.perform_later(@patient.id)
  end
end

Mock AI Scribe Service

# app/services/mock_ai_scribe_service.rb
class MockAiScribeService
  # Mock data for demonstration
  MOCK_SYMPTOMS = [
    'headache', 'fever', 'cough', 'fatigue', 'nausea', 
    'chest pain', 'shortness of breath', 'dizziness'
  ]
  
  MOCK_MEDICATIONS = [
    { name: 'Paracetamol', dosage: '500mg', frequency: 'TID' },
    { name: 'Amoxicillin', dosage: '250mg', frequency: 'BID' },
    { name: 'Omeprazole', dosage: '20mg', frequency: 'OD' },
    { name: 'Metformin', dosage: '500mg', frequency: 'BID' }
  ]
  
  MOCK_DIAGNOSES = [
    { name: 'Upper Respiratory Tract Infection', icd10: 'J06.9' },
    { name: 'Essential Hypertension', icd10: 'I10' },
    { name: 'Type 2 Diabetes Mellitus', icd10: 'E11.9' },
    { name: 'Acute Pharyngitis', icd10: 'J02.9' }
  ]
  
  def process_audio(encounter_id, audio_data = nil)
    encounter = Encounter.find(encounter_id)
    
    # Simulate processing delay
    sleep(1) if Rails.env.development?
    
    # Generate mock transcript
    transcript = generate_mock_transcript(encounter)
    
    # Extract entities
    entities = extract_mock_entities(transcript)
    
    # Generate suggestions
    suggestions = generate_mock_suggestions(entities, encounter)
    
    # Save transcription
    ai_transcription = AiTranscription.create!(
      encounter: encounter,
      raw_transcript: transcript,
      extracted_entities: entities,
      suggestions: suggestions,
      confidence_level: 'high',
      processing_status: 'completed',
      processing_duration: rand(2.0..5.0)
    )
    
    ai_transcription
  end
  
  def get_suggestions(encounter_id)
    transcription = AiTranscription.find_by(encounter_id: encounter_id)
    return nil unless transcription
    
    transcription.suggestions
  end
  
  def accept_suggestion(encounter_id, suggestion_type, suggestion_id)
    transcription = AiTranscription.find_by(encounter_id: encounter_id)
    return false unless transcription
    
    # Update acceptance tracking
    acceptance_details = transcription.acceptance_details || {}
    acceptance_details[suggestion_type] ||= {}
    acceptance_details[suggestion_type][suggestion_id] = {
      accepted: true,
      accepted_at: Time.current,
      accepted_by: Current.user&.id
    }
    
    transcription.update!(
      acceptance_details: acceptance_details,
      accepted_by_doctor: true
    )
    
    true
  end
  
  private
  
  def generate_mock_transcript(encounter)
    patient = encounter.patient
    complaints = encounter.encounter_complaints.pluck(:name).join(', ')
    
    transcript = "Patient presents with #{complaints.presence || 'general health concerns'}. "
    
    # Add mock vital signs narration
    if rand > 0.5
      transcript += "Blood pressure is 120 over 80. Heart rate is 72 beats per minute. "
      transcript += "Temperature is 98.6 degrees Fahrenheit. "
    end
    
    # Add mock symptom description
    symptoms = MOCK_SYMPTOMS.sample(rand(2..4))
    transcript += "Patient reports #{symptoms.join(', ')}. "
    
    # Add mock examination findings
    transcript += "On examination, patient appears comfortable. "
    transcript += "Chest is clear to auscultation bilaterally. " if rand > 0.6
    transcript += "Abdomen is soft and non-tender. " if rand > 0.5
    
    # Add mock assessment
    transcript += "Assessment suggests #{MOCK_DIAGNOSES.sample[:name].downcase}. "
    
    # Add mock plan
    transcript += "Recommend symptomatic treatment and follow-up if symptoms persist."
    
    transcript
  end
  
  def extract_mock_entities(transcript)
    {
      symptoms: extract_symptoms_from_transcript(transcript),
      vitals: extract_vitals_from_transcript(transcript),
      medications: extract_medications_from_transcript(transcript),
      physical_exam: extract_exam_findings(transcript)
    }
  end
  
  def extract_symptoms_from_transcript(transcript)
    found_symptoms = []
    
    MOCK_SYMPTOMS.each do |symptom|
      if transcript.downcase.include?(symptom.downcase)
        found_symptoms << {
          name: symptom,
          severity: %w[mild moderate severe].sample,
          duration: "#{rand(1..7)} days"
        }
      end
    end
    
    found_symptoms
  end
  
  def extract_vitals_from_transcript(transcript)
    vitals = {}
    
    # Extract blood pressure
    if transcript =~ /(\d+)\s*(?:over|\/)\s*(\d+)/
      vitals[:blood_pressure] = {
        systolic: $1.to_i,
        diastolic: $2.to_i
      }
    end
    
    # Extract heart rate
    if transcript =~ /(\d+)\s*beats?\s*per\s*minute/i
      vitals[:heart_rate] = $1.to_i
    end
    
    # Extract temperature
    if transcript =~ /(\d+\.?\d*)\s*degrees/i
      vitals[:temperature] = $1.to_f
    end
    
    vitals
  end
  
  def extract_medications_from_transcript(transcript)
    medications = []
    
    MOCK_MEDICATIONS.each do |med|
      if transcript.downcase.include?(med[:name].downcase)
        medications << med
      end
    end
    
    medications
  end
  
  def extract_exam_findings(transcript)
    findings = []
    
    exam_patterns = [
      'chest is clear',
      'abdomen is soft',
      'appears comfortable',
      'no distress'
    ]
    
    exam_patterns.each do |pattern|
      if transcript.downcase.include?(pattern)
        findings << pattern
      end
    end
    
    findings
  end
  
  def generate_mock_suggestions(entities, encounter)
    suggestions = {
      diagnoses: generate_diagnosis_suggestions(entities),
      medications: generate_medication_suggestions(entities),
      tests: generate_test_suggestions(entities),
      clinical_notes: generate_note_suggestions(entities)
    }
    
    suggestions
  end
  
  def generate_diagnosis_suggestions(entities)
    suggestions = []
    
    # Generate 1-3 diagnosis suggestions based on symptoms
    num_suggestions = rand(1..3)
    
    MOCK_DIAGNOSES.sample(num_suggestions).each do |diagnosis|
      suggestions << {
        id: SecureRandom.uuid,
        name: diagnosis[:name],
        icd10_code: diagnosis[:icd10],
        confidence: rand(0.65..0.95).round(2),
        supporting_evidence: entities[:symptoms].map { |s| s[:name] }
      }
    end
    
    suggestions
  end
  
  def generate_medication_suggestions(entities)
    suggestions = []
    
    # Generate medication suggestions
    MOCK_MEDICATIONS.sample(rand(1..3)).each do |med|
      suggestions << {
        id: SecureRandom.uuid,
        medication_name: med[:name],
        dosage: med[:dosage],
        frequency: med[:frequency],
        duration: "#{rand(3..7)} days",
        route: 'oral',
        confidence: rand(0.70..0.90).round(2)
      }
    end
    
    suggestions
  end
  
  def generate_test_suggestions(entities)
    tests = [
      { name: 'Complete Blood Count', code: 'CBC' },
      { name: 'Basic Metabolic Panel', code: 'BMP' },
      { name: 'Chest X-Ray', code: 'CXR' },
      { name: 'Urinalysis', code: 'UA' }
    ]
    
    suggestions = []
    
    tests.sample(rand(0..2)).each do |test|
      suggestions << {
        id: SecureRandom.uuid,
        test_name: test[:name],
        test_code: test[:code],
        reason: 'Rule out infection',
        confidence: rand(0.60..0.85).round(2)
      }
    end
    
    suggestions
  end
  
  def generate_note_suggestions(entities)
    note = "CHIEF COMPLAINT:\n"
    note += entities[:symptoms].map { |s| s[:name] }.join(', ') + "\n\n"
    
    note += "VITAL SIGNS:\n"
    if entities[:vitals][:blood_pressure]
      note += "BP: #{entities[:vitals][:blood_pressure][:systolic]}/#{entities[:vitals][:blood_pressure][:diastolic]}\n"
    end
    note += "HR: #{entities[:vitals][:heart_rate]}\n" if entities[:vitals][:heart_rate]
    note += "\n"
    
    note += "PHYSICAL EXAMINATION:\n"
    entities[:physical_exam].each { |finding| note += "- #{finding.capitalize}\n" }
    
    {
      content: note,
      confidence: 0.85
    }
  end
end

API Layer

Controllers

# app/controllers/api/encounters_controller.rb
class Api::EncountersController < ApiController
  before_action :policy_check
  before_action :set_encounter, only: [:show, :update, :reconcile_medications, :process_transcription]
  
  # GET /api/encounters
  def index
    result = Encounters::Index.call(params)
    render_result(result, EncounterSerializer)
  end
  
  # POST /api/encounters
  def create
    result = Encounters::Create.call(params)
    render_result(result, EncounterSerializer)
  end
  
  # GET /api/encounters/:id
  def show
    render json: EncounterSerializer.new(@encounter, include: [:patient_problems, :medication_prescriptions])
  end
  
  # PATCH /api/encounters/:id
  def update
    result = Encounters::Update.call(params.merge(encounter: @encounter))
    render_result(result, EncounterSerializer)
  end
  
  # POST /api/encounters/:id/reconcile_medications
  def reconcile_medications
    service = MedicationReconciliationService.new(@encounter)
    reconciliation = service.perform_reconciliation
    
    render json: { 
      status: 'success',
      reconciliation: reconciliation 
    }
  end
  
  # POST /api/encounters/:id/process_transcription
  def process_transcription
    service = MockAiScribeService.new
    transcription = service.process_audio(@encounter.id, params[:audio_data])
    
    render json: AiTranscriptionSerializer.new(transcription)
  end
  
  # GET /api/encounters/:id/ai_suggestions
  def ai_suggestions
    service = MockAiScribeService.new
    suggestions = service.get_suggestions(@encounter.id)
    
    if suggestions
      render json: { status: 'success', suggestions: suggestions }
    else
      render json: { status: 'not_found', message: 'No AI suggestions available' }
    end
  end
  
  private
  
  def set_encounter
    @encounter = Encounter.find(params[:id])
  end
  
  def policy_check
    authorize params, policy_class: EncountersControllerPolicy
  end
end

# app/controllers/api/longitudinal_health_controller.rb
class Api::LongitudinalHealthController < ApiController
  before_action :policy_check
  before_action :set_patient
  
  # GET /api/patients/:patient_id/health_summary
  def health_summary
    service = LongitudinalHealthService.new(@patient.id)
    summary = service.build_complete_record
    
    render json: { 
      status: 'success',
      data: summary,
      generated_at: Time.current
    }
  end
  
  # GET /api/patients/:patient_id/health_score
  def health_score
    service = LongitudinalHealthService.new(@patient.id)
    score = service.calculate_health_score
    
    render json: PatientHealthScoreSerializer.new(score)
  end
  
  # GET /api/patients/:patient_id/vital_trends
  def vital_trends
    days_back = params[:days] || 90
    trends = VitalTrendService.new(@patient).calculate_trends(days_back: days_back)
    
    render json: { 
      status: 'success',
      trends: trends,
      period: "#{days_back} days"
    }
  end
  
  # GET /api/patients/:patient_id/encounter_timeline
  def encounter_timeline
    encounters = @patient.encounters
                        .includes(:doctor, :patient_problems)
                        .recent
                        .page(params[:page])
    
    render json: EncounterSerializer.new(encounters, 
      meta: pagination_meta(encounters))
  end
  
  private
  
  def set_patient
    @patient = Patient.find(params[:patient_id])
  end
  
  def policy_check
    authorize params, policy_class: LongitudinalHealthControllerPolicy
  end
end

# app/controllers/api/patient_problems_controller.rb
class Api::PatientProblemsController < ApiController
  before_action :policy_check
  before_action :set_patient
  before_action :set_problem, only: [:show, :update, :resolve]
  
  # GET /api/patients/:patient_id/problems
  def index
    problems = @patient.patient_problems
                      .includes(:created_by_encounter, :created_by_doctor)
                      .by_severity
    
    render json: PatientProblemSerializer.new(problems)
  end
  
  # POST /api/patients/:patient_id/problems
  def create
    problem = @patient.patient_problems.build(problem_params)
    problem.created_by_doctor = current_doctor
    problem.created_by_encounter_id = params[:encounter_id] if params[:encounter_id]
    
    if problem.save
      render json: PatientProblemSerializer.new(problem), status: :created
    else
      render json: { errors: problem.errors.full_messages }, status: :unprocessable_entity
    end
  end
  
  # PATCH /api/patient_problems/:id
  def update
    if @problem.update(problem_params)
      render json: PatientProblemSerializer.new(@problem)
    else
      render json: { errors: @problem.errors.full_messages }, status: :unprocessable_entity
    end
  end
  
  # POST /api/patient_problems/:id/resolve
  def resolve
    @problem.update!(
      status: 'resolved',
      resolved_date: Date.current,
      resolved_by_encounter_id: params[:encounter_id]
    )
    
    render json: PatientProblemSerializer.new(@problem)
  end
  
  private
  
  def set_patient
    @patient = Patient.find(params[:patient_id])
  end
  
  def set_problem
    @problem = PatientProblem.find(params[:id])
  end
  
  def problem_params
    params.require(:patient_problem).permit(
      :name, :icd10_code, :icd11_code, :snomed_code,
      :status, :severity, :onset_date, :clinical_notes
    )
  end
  
  def policy_check
    authorize params, policy_class: PatientProblemsControllerPolicy
  end
end

# app/controllers/api/medication_prescriptions_controller.rb
class Api::MedicationPrescriptionsController < ApiController
  before_action :policy_check
  before_action :set_patient
  before_action :set_prescription, only: [:show, :update, :discontinue, :refill]
  
  # GET /api/patients/:patient_id/current_medications
  def current_medications
    medications = @patient.medication_prescriptions
                         .active
                         .includes(:doctor, :encounter)
    
    render json: MedicationPrescriptionSerializer.new(medications)
  end
  
  # GET /api/patients/:patient_id/medication_history
  def medication_history
    medications = @patient.medication_prescriptions
                         .includes(:doctor, :encounter)
                         .order(created_at: :desc)
                         .page(params[:page])
    
    render json: MedicationPrescriptionSerializer.new(medications,
      meta: pagination_meta(medications))
  end
  
  # POST /api/encounters/:encounter_id/prescriptions
  def create
    encounter = Encounter.find(params[:encounter_id])
    prescription = encounter.medication_prescriptions.build(prescription_params)
    prescription.patient = encounter.patient
    prescription.doctor = encounter.doctor
    
    if prescription.save
      render json: MedicationPrescriptionSerializer.new(prescription), status: :created
    else
      render json: { errors: prescription.errors.full_messages }, status: :unprocessable_entity
    end
  end
  
  # POST /api/prescriptions/:id/discontinue
  def discontinue
    @prescription.update!(
      status: 'discontinued',
      discontinuation_reason: params[:reason],
      discontinued_by_encounter_id: params[:encounter_id],
      end_date: Date.current
    )
    
    render json: MedicationPrescriptionSerializer.new(@prescription)
  end
  
  # POST /api/prescriptions/:id/refill
  def refill
    refill = @prescription.refills.build(
      @prescription.attributes.except('id', 'created_at', 'updated_at')
    )
    refill.parent_prescription = @prescription
    refill.prescribed_date = Date.current
    refill.start_date = Date.current
    refill.encounter_id = params[:encounter_id]
    
    if refill.save
      render json: MedicationPrescriptionSerializer.new(refill), status: :created
    else
      render json: { errors: refill.errors.full_messages }, status: :unprocessable_entity
    end
  end
  
  private
  
  def set_patient
    @patient = Patient.find(params[:patient_id])
  end
  
  def set_prescription
    @prescription = MedicationPrescription.find(params[:id])
  end
  
  def prescription_params
    params.require(:prescription).permit(
      :medication_name, :generic_name, :snomed_medication_code,
      :dosage, :frequency, :route, :duration_value, :duration_unit,
      :prescription_type, :start_date, :instructions
    )
  end
  
  def policy_check
    authorize params, policy_class: MedicationPrescriptionsControllerPolicy
  end
end

Backward Compatibility Layer

# app/controllers/api/careplans_controller.rb
# Maintains backward compatibility with existing frontend
class Api::CareplansController < Api::EncountersController
  # All methods inherited from EncountersController
  # This ensures existing API calls to /api/careplans continue to work
end

Routes Configuration

# config/routes.rb (additions)
Rails.application.routes.draw do
  namespace :api, defaults: { format: :json } do
    # Backward compatibility routes
    resources :careplans do
      member do
        post :reconcile_medications
        post :process_transcription
        get :ai_suggestions
      end
    end
    
    # New encounter routes (same functionality)
    resources :encounters do
      member do
        post :reconcile_medications
        post :process_transcription
        get :ai_suggestions
      end
      
      resources :prescriptions, controller: 'medication_prescriptions', only: [:create]
    end
    
    # Longitudinal health routes
    resources :patients do
      # Health summary and scores
      get :health_summary, controller: 'longitudinal_health'
      get :health_score, controller: 'longitudinal_health'
      get :vital_trends, controller: 'longitudinal_health'
      get :encounter_timeline, controller: 'longitudinal_health'
      
      # Problem management
      resources :problems, controller: 'patient_problems' do
        member do
          post :resolve
        end
      end
      
      # Medication management
      get :current_medications, controller: 'medication_prescriptions'
      get :medication_history, controller: 'medication_prescriptions'
    end
    
    # Prescription management
    resources :prescriptions, controller: 'medication_prescriptions', only: [] do
      member do
        post :discontinue
        post :refill
      end
    end
    
    # ABDM FHIR endpoints
    namespace :abdm do
      resources :patients, only: [] do
        member do
          get :fhir_bundle
          post :sync_health_records
        end
      end
    end
    
    # Clinical codes lookup
    resources :clinical_codes, only: [:index] do
      collection do
        get :search
      end
    end
  end
end

Implementation Guide

Phase 1: Database & Models

  1. Create migration files in order
  2. Run migrations: rails db:migrate
  3. Create model files with associations
  4. Add validations and callbacks
  5. Test model relationships in console

Phase 2: Service Layer

  1. Implement LongitudinalHealthService
  2. Create MedicationReconciliationService
  3. Build MockAiScribeService
  4. Add FHIR bundle generator
  5. Create background jobs for async processing

Phase 3: API Controllers

  1. Rename CareplansController to EncountersController
  2. Create backward compatibility wrapper
  3. Implement new longitudinal controllers
  4. Add serializers for new models
  5. Test API endpoints

Phase 4: Data Migration

  1. Create rake task for data migration
  2. Rename existing careplan references
  3. Extract problems from diagnosis fields
  4. Build initial health summaries
  5. Generate health scores for existing patients

Phase 5: Integration Testing

  1. Test backward compatibility
  2. Verify new API endpoints
  3. Test medication reconciliation flow
  4. Validate health score calculations
  5. Test AI mock service

Migration Strategy

Data Migration Rake Task

# lib/tasks/migrate_to_longitudinal.rake
namespace :longitudinal do
  desc "Migrate existing careplan data to longitudinal health record structure"
  task migrate: :environment do
    puts "Starting longitudinal health record migration..."
    
    ActiveRecord::Base.transaction do
      # 1. Update polymorphic references
      update_polymorphic_references
      
      # 2. Extract problems from existing diagnoses
      extract_patient_problems
      
      # 3. Convert existing medicines to prescriptions
      convert_medicines_to_prescriptions
      
      # 4. Generate initial health summaries
      generate_health_summaries
      
      # 5. Calculate initial health scores
      calculate_initial_scores
    end
    
    puts "Migration completed successfully!"
  end
  
  private
  
  def update_polymorphic_references
    puts "Updating polymorphic references..."
    
    # Update documents
    Document.where(documentable_type: 'Careplan')
           .update_all(documentable_type: 'Encounter')
    
    # Update activities
    PublicActivity::Activity.where(trackable_type: 'Careplan')
                           .update_all(trackable_type: 'Encounter')
  end
  
  def extract_patient_problems
    puts "Extracting patient problems from diagnoses..."
    
    Encounter.find_each do |encounter|
      next if encounter.diagnosis.blank?
      
      encounter.diagnosis.each do |diagnosis_text|
        # Try to find or create a problem
        problem = encounter.patient.patient_problems.find_or_create_by(
          name: diagnosis_text,
          created_by_encounter: encounter,
          created_by_doctor: encounter.doctor
        ) do |p|
          p.status = 'active'
          p.onset_date = encounter.created_at.to_date
          # Try to match with ICD codes from clinical_codes table
          code = ClinicalCode.where("display_name ILIKE ?", "%#{diagnosis_text}%").first
          if code
            p.icd10_code = code.code if code.code_system == 'ICD10'
          end
        end
        
        # Link to encounter
        EncounterProblem.find_or_create_by(
          encounter: encounter,
          patient_problem: problem,
          relevance: 'primary'
        )
      end
    end
  end
  
  def convert_medicines_to_prescriptions
    puts "Converting existing medicines to prescriptions..."
    
    EncounterMedicine.includes(:encounter).find_each do |medicine|
      MedicationPrescription.create!(
        patient: medicine.encounter.patient,
        clinic_patient: medicine.encounter.clinic_patient,
        encounter: medicine.encounter,
        doctor: medicine.encounter.doctor,
        medication_name: medicine.name,
        dosage: medicine.dosage,
        frequency: medicine.frequency,
        duration_value: medicine.duration,
        duration_unit: medicine.duration_type,
        prescribed_date: medicine.encounter.created_at.to_date,
        start_date: medicine.encounter.created_at.to_date,
        status: 'completed', # Assume old prescriptions are completed
        prescription_type: 'acute'
      )
    end
  end
  
  def generate_health_summaries
    puts "Generating patient health summaries..."
    
    Patient.find_each do |patient|
      next if patient.encounters.empty?
      
      PatientHealthSummary.create!(
        patient: patient,
        clinic_patient: patient.clinic_patients.last,
        total_encounters_count: patient.encounters.count,
        last_encounter_date: patient.encounters.maximum(:created_at),
        created_at: Time.current,
        updated_at: Time.current
      )
    end
  end
  
  def calculate_initial_scores
    puts "Calculating initial health scores..."
    
    Patient.find_each do |patient|
      next if patient.encounters.empty?
      
      service = LongitudinalHealthService.new(patient.id)
      service.calculate_health_score
    end
  end
end

Monitoring & Metrics

Key Performance Indicators

  • Health score calculation time < 500ms
  • Medication reconciliation completion rate > 90%
  • AI transcription mock response time < 2s
  • Problem list accuracy > 95%
  • API backward compatibility 100%

Monitoring Setup

# config/initializers/metrics.rb
Rails.application.config.after_initialize do
  # Track health score calculations
  ActiveSupport::Notifications.subscribe('health_score.calculated') do |*args|
    event = ActiveSupport::Notifications::Event.new(*args)
    Rails.logger.info "[METRICS] Health score calculated in #{event.duration}ms"
  end
  
  # Track medication reconciliations
  ActiveSupport::Notifications.subscribe('medication.reconciled') do |*args|
    event = ActiveSupport::Notifications::Event.new(*args)
    Rails.logger.info "[METRICS] Medication reconciled in #{event.duration}ms"
  end
end

Security Considerations

Access Control

  • Implement role-based access for longitudinal records
  • Audit all access to patient health summaries
  • Encrypt sensitive health scores at rest
  • Implement patient consent management

HIPAA Compliance

  • Log all PHI access
  • Implement automatic session timeouts
  • Encrypt all data in transit
  • Regular security audits

Future Enhancements

Phase 2 Features

  • Real AI integration (OpenAI/AWS Transcribe)
  • Medication adherence tracking
  • Predictive health analytics
  • Patient portal access
  • Mobile app integration

Phase 3 Features

  • Advanced clinical decision support
  • Population health analytics
  • Quality measure reporting
  • Interoperability with other EHRs
  • Telemedicine integration

Conclusion

This architecture provides a robust foundation for longitudinal health records while maintaining backward compatibility. The modular design allows for incremental implementation and future enhancements without disrupting existing functionality.

On this page