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_recordGET /clinics/:clinic_id/patients/:clinic_patient_id/problemsGET /clinics/:clinic_id/patients/:clinic_patient_id/medications_timelineGET /clinics/:clinic_id/patients/:clinic_patient_id/clinical_assessmentsGET /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
1.5 Link Medications to Problem
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
endRoutes 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'
endInteractor 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
endImplementation Priority
Phase 1: Core Problem Management (Days 1-2)
CreatePatientProbleminteractor and endpointUpdatePatientProbleminteractor and endpointDeletePatientProbleminteractor and endpoint- Database migrations for PatientProblem enhancements
Phase 2: Problem Resolution & Linking (Day 3)
ResolvePatientProbleminteractor and endpointLinkProblemMedicationsinteractor and endpoint- Database migrations for medication linking
Phase 3: Medication Management (Day 4)
GetMedicationDetailsinteractor and endpointUpdateMedicationStatusinteractor and endpoint- Database migrations for medication status history
Phase 4: Encounter Management (Day 5)
GetEncounterDetailsinteractor and endpointScheduleFollowUpAppointmentinteractor and endpoint- 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
endError Handling Strategy
Standard Error Responses
{
"success": false,
"error": "Problem name is required",
"error_code": "VALIDATION_ERROR"
}HTTP Status Codes
200- Success201- Created400- Bad Request (validation errors)401- Unauthorized403- Forbidden (authorization)404- Not Found422- 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.