Kulcare Docs
DeveloperCare plansCare planE prescription

E-Prescription Technical Specification

System Architecture

High-Level Architecture

┌─────────────────────────────────────────────────────────────────┐
│                         Frontend (React)                         │
│  ┌──────────────┬───────────────┬──────────────┬─────────────┐ │
│  │ Prescription │ Drug Search   │ Pharmacy     │ Clinical    │ │
│  │ Form         │ Component     │ Selector     │ Alerts      │ │
│  └──────────────┴───────────────┴──────────────┴─────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│                      API Gateway (Rails)                         │
│  ┌──────────────────────────────────────────────────────────┐  │
│  │ Authentication │ Authorization │ Rate Limiting │ Logging  │  │
│  └──────────────────────────────────────────────────────────┘  │
├─────────────────────────────────────────────────────────────────┤
│                    Application Layer (Rails)                     │
│  ┌─────────────┬──────────────┬──────────────┬──────────────┐  │
│  │ Prescription│ Drug         │ Pharmacy     │ Clinical     │  │
│  │ Service    │ Service      │ Service      │ Decision     │  │
│  └─────────────┴──────────────┴──────────────┴──────────────┘  │
├─────────────────────────────────────────────────────────────────┤
│                    Integration Layer                             │
│  ┌─────────────┬──────────────┬──────────────┬──────────────┐  │
│  │ Surescripts │ RxNorm       │ PDMP         │ First        │  │
│  │ Adapter     │ Adapter      │ Adapter      │ Databank     │  │
│  └─────────────┴──────────────┴──────────────┴──────────────┘  │
├─────────────────────────────────────────────────────────────────┤
│                      Data Layer                                  │
│  ┌─────────────┬──────────────┬──────────────┬──────────────┐  │
│  │ PostgreSQL  │ Redis        │ Elasticsearch│ S3           │  │
│  │ (Primary)   │ (Cache/Queue)│ (Search)     │ (Documents)  │  │
│  └─────────────┴──────────────┴──────────────┴──────────────┘  │
└─────────────────────────────────────────────────────────────────┘

Database Schema

Core Tables

-- Electronic Prescriptions
CREATE TABLE electronic_prescriptions (
  id BIGSERIAL PRIMARY KEY,
  encounter_id BIGINT REFERENCES encounters(id),
  patient_id BIGINT NOT NULL REFERENCES patients(id),
  clinic_patient_id BIGINT REFERENCES clinic_patients(id),
  doctor_id BIGINT NOT NULL REFERENCES doctors(id),
  clinic_id BIGINT NOT NULL REFERENCES clinics(id),
  
  -- Prescription Details
  prescription_guid UUID NOT NULL DEFAULT gen_random_uuid(),
  prescription_date DATE NOT NULL,
  written_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  
  -- Medication Information
  medication_name VARCHAR(255) NOT NULL,
  medication_generic_name VARCHAR(255),
  rxnorm_code VARCHAR(20),
  ndc_code VARCHAR(20),
  medication_form VARCHAR(100),
  medication_strength VARCHAR(100),
  
  -- Dosing Instructions
  dosage VARCHAR(100) NOT NULL,
  dosage_unit VARCHAR(50),
  frequency VARCHAR(100) NOT NULL,
  route VARCHAR(50) NOT NULL,
  duration_value INTEGER,
  duration_unit VARCHAR(20),
  quantity_prescribed DECIMAL(10,2) NOT NULL,
  quantity_unit VARCHAR(50),
  
  -- Prescription Options
  refills_authorized INTEGER DEFAULT 0,
  refills_remaining INTEGER DEFAULT 0,
  generic_substitution_allowed BOOLEAN DEFAULT true,
  is_compound BOOLEAN DEFAULT false,
  is_controlled BOOLEAN DEFAULT false,
  schedule_class VARCHAR(10), -- II, III, IV, V
  
  -- SIG (Patient Instructions)
  sig_text TEXT NOT NULL,
  sig_codes JSONB, -- Structured SIG codes
  patient_notes TEXT,
  
  -- Pharmacy Information
  pharmacy_id BIGINT REFERENCES pharmacies(id),
  pharmacy_ncpdp_id VARCHAR(20),
  
  -- Status Tracking
  status VARCHAR(50) NOT NULL DEFAULT 'draft',
  -- draft, queued, sent, received, dispensed, cancelled, failed
  
  -- Transmission Details
  transmission_method VARCHAR(50), -- electronic, print, fax
  transmitted_at TIMESTAMP,
  received_at TIMESTAMP,
  dispensed_at TIMESTAMP,
  cancelled_at TIMESTAMP,
  cancellation_reason TEXT,
  
  -- Clinical Decision Support
  drug_interactions JSONB,
  allergy_alerts JSONB,
  clinical_alerts JSONB,
  override_reasons JSONB,
  
  -- Audit Fields
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  created_by_user_id BIGINT REFERENCES users(id),
  updated_by_user_id BIGINT REFERENCES users(id),
  
  CONSTRAINT unique_prescription_guid UNIQUE (prescription_guid)
);

-- Indexes for performance
CREATE INDEX idx_prescriptions_patient ON electronic_prescriptions(patient_id);
CREATE INDEX idx_prescriptions_doctor ON electronic_prescriptions(doctor_id);
CREATE INDEX idx_prescriptions_status ON electronic_prescriptions(status);
CREATE INDEX idx_prescriptions_date ON electronic_prescriptions(prescription_date);
CREATE INDEX idx_prescriptions_pharmacy ON electronic_prescriptions(pharmacy_id);

-- Pharmacies Directory
CREATE TABLE pharmacies (
  id BIGSERIAL PRIMARY KEY,
  ncpdp_id VARCHAR(20) NOT NULL UNIQUE,
  npi_number VARCHAR(20),
  
  -- Basic Information
  name VARCHAR(255) NOT NULL,
  doing_business_as VARCHAR(255),
  chain_code VARCHAR(20),
  
  -- Address
  address_line_1 VARCHAR(255) NOT NULL,
  address_line_2 VARCHAR(255),
  city VARCHAR(100) NOT NULL,
  state VARCHAR(2) NOT NULL,
  zip_code VARCHAR(10) NOT NULL,
  country VARCHAR(2) DEFAULT 'US',
  latitude DECIMAL(10, 8),
  longitude DECIMAL(11, 8),
  
  -- Contact Information
  phone_primary VARCHAR(20) NOT NULL,
  phone_secondary VARCHAR(20),
  fax VARCHAR(20),
  email VARCHAR(255),
  
  -- Service Information
  is_24_hours BOOLEAN DEFAULT false,
  has_delivery BOOLEAN DEFAULT false,
  has_compounding BOOLEAN DEFAULT false,
  has_drive_through BOOLEAN DEFAULT false,
  is_mail_order BOOLEAN DEFAULT false,
  is_specialty BOOLEAN DEFAULT false,
  
  -- Hours of Operation
  hours_of_operation JSONB,
  /* Format:
  {
    "monday": {"open": "09:00", "close": "21:00"},
    "tuesday": {"open": "09:00", "close": "21:00"},
    ...
  }
  */
  
  -- Network Status
  surescripts_enabled BOOLEAN DEFAULT true,
  active BOOLEAN DEFAULT true,
  last_verified_date DATE,
  
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- Indexes for pharmacy search
CREATE INDEX idx_pharmacy_name ON pharmacies(name);
CREATE INDEX idx_pharmacy_zip ON pharmacies(zip_code);
CREATE INDEX idx_pharmacy_city_state ON pharmacies(city, state);
CREATE INDEX idx_pharmacy_location ON pharmacies USING GIST (
  ll_to_earth(latitude, longitude)
);

-- Prescription Templates
CREATE TABLE prescription_templates (
  id BIGSERIAL PRIMARY KEY,
  doctor_id BIGINT REFERENCES doctors(id),
  clinic_id BIGINT REFERENCES clinics(id),
  
  -- Template Information
  template_name VARCHAR(255) NOT NULL,
  category VARCHAR(100),
  is_system_template BOOLEAN DEFAULT false,
  is_active BOOLEAN DEFAULT true,
  
  -- Medication Details (same as prescription)
  medication_name VARCHAR(255) NOT NULL,
  medication_generic_name VARCHAR(255),
  rxnorm_code VARCHAR(20),
  medication_form VARCHAR(100),
  medication_strength VARCHAR(100),
  
  -- Default Dosing
  dosage VARCHAR(100),
  frequency VARCHAR(100),
  route VARCHAR(50),
  duration_value INTEGER,
  duration_unit VARCHAR(20),
  quantity_prescribed DECIMAL(10,2),
  refills_authorized INTEGER DEFAULT 0,
  
  -- Default SIG
  sig_text TEXT,
  
  -- Usage Tracking
  usage_count INTEGER DEFAULT 0,
  last_used_at TIMESTAMP,
  
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- Patient Favorite Pharmacies
CREATE TABLE patient_favorite_pharmacies (
  id BIGSERIAL PRIMARY KEY,
  patient_id BIGINT NOT NULL REFERENCES patients(id),
  pharmacy_id BIGINT NOT NULL REFERENCES pharmacies(id),
  is_primary BOOLEAN DEFAULT false,
  nickname VARCHAR(100),
  last_used_at TIMESTAMP,
  usage_count INTEGER DEFAULT 0,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  
  CONSTRAINT unique_patient_pharmacy UNIQUE (patient_id, pharmacy_id)
);

-- Prescription Transmission Log
CREATE TABLE prescription_transmissions (
  id BIGSERIAL PRIMARY KEY,
  prescription_id BIGINT NOT NULL REFERENCES electronic_prescriptions(id),
  
  -- Transmission Details
  transmission_id UUID DEFAULT gen_random_uuid(),
  message_type VARCHAR(50) NOT NULL, -- NewRx, CancelRx, RefillRequest, etc.
  message_version VARCHAR(20) NOT NULL, -- NCPDP SCRIPT version
  
  -- Request/Response
  request_payload TEXT NOT NULL,
  response_payload TEXT,
  
  -- Status
  transmission_status VARCHAR(50) NOT NULL,
  -- pending, sent, acknowledged, rejected, timeout, failed
  
  -- Error Handling
  error_code VARCHAR(50),
  error_message TEXT,
  retry_count INTEGER DEFAULT 0,
  max_retries INTEGER DEFAULT 3,
  
  -- Timing
  sent_at TIMESTAMP,
  acknowledged_at TIMESTAMP,
  completed_at TIMESTAMP,
  
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- Clinical Alerts Override Log
CREATE TABLE prescription_override_log (
  id BIGSERIAL PRIMARY KEY,
  prescription_id BIGINT NOT NULL REFERENCES electronic_prescriptions(id),
  doctor_id BIGINT NOT NULL REFERENCES doctors(id),
  
  -- Alert Details
  alert_type VARCHAR(50) NOT NULL, -- drug_interaction, allergy, dosage, duplicate
  alert_severity VARCHAR(20) NOT NULL, -- high, medium, low
  alert_message TEXT NOT NULL,
  
  -- Override Information
  override_reason TEXT NOT NULL,
  override_code VARCHAR(50),
  
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- Drug Database Cache
CREATE TABLE drug_database (
  id BIGSERIAL PRIMARY KEY,
  rxnorm_code VARCHAR(20) UNIQUE,
  ndc_code VARCHAR(20),
  
  -- Drug Information
  brand_name VARCHAR(255),
  generic_name VARCHAR(255),
  drug_class VARCHAR(100),
  
  -- Formulation
  dosage_form VARCHAR(100),
  strength VARCHAR(100),
  strength_unit VARCHAR(50),
  route VARCHAR(50),
  
  -- Classification
  is_controlled BOOLEAN DEFAULT false,
  dea_schedule VARCHAR(10),
  is_generic BOOLEAN,
  is_otc BOOLEAN DEFAULT false,
  
  -- Metadata
  manufacturer VARCHAR(255),
  package_size VARCHAR(100),
  
  -- Search Optimization
  search_terms TEXT[], -- Array of searchable terms
  
  -- Data Source
  data_source VARCHAR(50), -- rxnorm, fdb, custom
  last_updated DATE,
  
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- Create full-text search index
CREATE INDEX idx_drug_search ON drug_database USING GIN(to_tsvector('english', 
  coalesce(brand_name, '') || ' ' || 
  coalesce(generic_name, '') || ' ' || 
  coalesce(array_to_string(search_terms, ' '), '')
));

-- Audit Log for Prescriptions
CREATE TABLE prescription_audit_log (
  id BIGSERIAL PRIMARY KEY,
  prescription_id BIGINT REFERENCES electronic_prescriptions(id),
  user_id BIGINT NOT NULL REFERENCES users(id),
  
  -- Action Details
  action VARCHAR(50) NOT NULL,
  -- create, view, modify, send, cancel, print, override_alert
  
  -- Context
  ip_address INET,
  user_agent TEXT,
  session_id VARCHAR(255),
  
  -- Changes
  changes_made JSONB,
  previous_values JSONB,
  
  -- Compliance
  is_controlled_substance BOOLEAN DEFAULT false,
  requires_two_factor BOOLEAN DEFAULT false,
  two_factor_verified BOOLEAN,
  
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- Indexes for audit compliance
CREATE INDEX idx_audit_prescription ON prescription_audit_log(prescription_id);
CREATE INDEX idx_audit_user ON prescription_audit_log(user_id);
CREATE INDEX idx_audit_date ON prescription_audit_log(created_at);
CREATE INDEX idx_audit_controlled ON prescription_audit_log(is_controlled_substance) 
  WHERE is_controlled_substance = true;

API Design

RESTful Endpoints

Prescription Management

# Create New Prescription
POST /api/v1/prescriptions
Request:
  {
    "prescription": {
      "patient_id": 123,
      "encounter_id": 456,
      "medication": {
        "name": "Amoxicillin",
        "rxnorm_code": "392151",
        "strength": "500mg",
        "form": "capsule"
      },
      "dosing": {
        "dosage": "1",
        "dosage_unit": "capsule",
        "frequency": "TID",
        "route": "oral",
        "duration_value": 10,
        "duration_unit": "days",
        "quantity": 30
      },
      "sig": "Take 1 capsule by mouth three times daily for 10 days",
      "refills": 0,
      "generic_substitution_allowed": true,
      "pharmacy_id": 789
    }
  }
Response:
  {
    "prescription": {
      "id": 1001,
      "prescription_guid": "550e8400-e29b-41d4-a716-446655440000",
      "status": "draft",
      "clinical_alerts": [
        {
          "type": "drug_interaction",
          "severity": "moderate",
          "message": "Potential interaction with Warfarin",
          "action_required": false
        }
      ]
    }
  }

# Send Prescription
POST /api/v1/prescriptions/:id/send
Response:
  {
    "transmission": {
      "id": 2001,
      "status": "sent",
      "transmission_id": "660e8400-e29b-41d4-a716-446655440000",
      "sent_at": "2024-03-15T10:30:00Z"
    }
  }

# Cancel Prescription
POST /api/v1/prescriptions/:id/cancel
Request:
  {
    "reason": "Medication changed per patient request"
  }

# Get Prescription Status
GET /api/v1/prescriptions/:id/status
Response:
  {
    "status": {
      "current": "received",
      "history": [
        {"status": "draft", "timestamp": "2024-03-15T10:00:00Z"},
        {"status": "sent", "timestamp": "2024-03-15T10:30:00Z"},
        {"status": "received", "timestamp": "2024-03-15T10:31:00Z"}
      ]
    }
  }

# List Prescriptions
GET /api/v1/prescriptions?patient_id=123&status=sent&date_from=2024-03-01
Response:
  {
    "prescriptions": [...],
    "meta": {
      "total": 45,
      "page": 1,
      "per_page": 20
    }
  }
# Search Drugs
GET /api/v1/drugs/search?q=amox&limit=10
Response:
  {
    "drugs": [
      {
        "rxnorm_code": "392151",
        "brand_name": "Amoxil",
        "generic_name": "Amoxicillin",
        "forms": [
          {"form": "capsule", "strengths": ["250mg", "500mg"]},
          {"form": "suspension", "strengths": ["125mg/5ml", "250mg/5ml"]}
        ]
      }
    ]
  }

# Get Drug Details
GET /api/v1/drugs/:rxnorm_code
Response:
  {
    "drug": {
      "rxnorm_code": "392151",
      "brand_names": ["Amoxil", "Trimox"],
      "generic_name": "Amoxicillin",
      "drug_class": "Penicillin Antibiotic",
      "dosage_forms": [...],
      "typical_dosing": [...],
      "is_controlled": false
    }
  }

# Check Drug Interactions
POST /api/v1/drugs/interactions
Request:
  {
    "medications": [
      {"rxnorm_code": "392151"},
      {"rxnorm_code": "855332"}
    ]
  }
Response:
  {
    "interactions": [
      {
        "severity": "moderate",
        "drugs": ["Amoxicillin", "Warfarin"],
        "description": "May increase INR",
        "recommendation": "Monitor INR closely"
      }
    ]
  }

Pharmacy Management

# Search Pharmacies
GET /api/v1/pharmacies/search?zip=10001&radius=5&limit=20
Response:
  {
    "pharmacies": [
      {
        "id": 789,
        "ncpdp_id": "1234567",
        "name": "CVS Pharmacy #0123",
        "address": "123 Main St, New York, NY 10001",
        "phone": "(212) 555-0123",
        "distance_miles": 0.5,
        "is_24_hours": false,
        "services": ["delivery", "compounding"]
      }
    ]
  }

# Get Pharmacy Details
GET /api/v1/pharmacies/:id
Response:
  {
    "pharmacy": {
      "id": 789,
      "hours": {
        "monday": {"open": "09:00", "close": "21:00"},
        ...
      }
    }
  }

# Save Favorite Pharmacy
POST /api/v1/patients/:patient_id/favorite_pharmacies
Request:
  {
    "pharmacy_id": 789,
    "is_primary": true
  }

Template Management

# List Templates
GET /api/v1/prescription_templates?category=antibiotics
Response:
  {
    "templates": [
      {
        "id": 101,
        "name": "Amoxicillin - Standard Adult",
        "category": "antibiotics",
        "medication_name": "Amoxicillin",
        "dosing": {...}
      }
    ]
  }

# Create Template
POST /api/v1/prescription_templates
Request:
  {
    "template": {
      "name": "My Amoxicillin Template",
      "category": "antibiotics",
      "medication": {...},
      "dosing": {...}
    }
  }

# Use Template
POST /api/v1/prescriptions/from_template
Request:
  {
    "template_id": 101,
    "patient_id": 123,
    "adjustments": {
      "quantity": 20
    }
  }

Service Layer Implementation

Prescription Service

# app/services/prescription_service.rb
class PrescriptionService
  include Interactor
  
  def call
    validate_provider_credentials
    validate_patient_eligibility
    check_drug_interactions
    check_allergies
    check_duplicate_therapy
    create_prescription
    queue_for_transmission if context.send_immediately
  end
  
  private
  
  def validate_provider_credentials
    provider = context.doctor
    
    unless provider.can_prescribe?
      context.fail!(error: "Provider not authorized to prescribe")
    end
    
    if context.medication.controlled? && !provider.dea_number.present?
      context.fail!(error: "DEA number required for controlled substances")
    end
  end
  
  def check_drug_interactions
    interactions = DrugInteractionService.call(
      patient: context.patient,
      new_medication: context.medication
    )
    
    if interactions.severe.any?
      context.clinical_alerts << {
        type: 'drug_interaction',
        severity: 'high',
        interactions: interactions.severe,
        action_required: true
      }
    end
  end
  
  def create_prescription
    context.prescription = ElectronicPrescription.create!(
      patient: context.patient,
      doctor: context.doctor,
      medication_name: context.medication.name,
      rxnorm_code: context.medication.rxnorm_code,
      dosage: context.dosing.dosage,
      frequency: context.dosing.frequency,
      quantity_prescribed: context.dosing.quantity,
      sig_text: context.sig,
      pharmacy_id: context.pharmacy_id,
      drug_interactions: context.clinical_alerts,
      status: 'draft'
    )
  end
end

Surescripts Integration Service

# app/services/surescripts_service.rb
class SurescriptsService
  ENDPOINT = ENV['SURESCRIPTS_ENDPOINT']
  
  def send_prescription(prescription)
    message = build_ncpdp_message(prescription)
    
    response = transmit_message(message)
    
    log_transmission(prescription, message, response)
    
    if response.success?
      prescription.update!(
        status: 'sent',
        transmitted_at: Time.current
      )
    else
      handle_transmission_failure(prescription, response)
    end
    
    response
  end
  
  private
  
  def build_ncpdp_message(prescription)
    NCPDPScript::NewRx.new(
      header: build_header(prescription),
      patient: build_patient_segment(prescription.patient),
      prescriber: build_prescriber_segment(prescription.doctor),
      pharmacy: build_pharmacy_segment(prescription.pharmacy),
      medication: build_medication_segment(prescription),
      sig: prescription.sig_text
    ).to_xml
  end
  
  def transmit_message(message)
    client = Faraday.new(url: ENDPOINT) do |conn|
      conn.request :authorization, 'Bearer', generate_token
      conn.request :json
      conn.response :json
      conn.adapter Faraday.default_adapter
    end
    
    response = client.post('/v2/prescriptions') do |req|
      req.headers['Content-Type'] = 'application/xml'
      req.body = message
    end
    
    SurescriptsResponse.new(response)
  rescue Faraday::TimeoutError => e
    queue_for_retry(message)
    raise TransmissionError, "Timeout sending prescription: #{e.message}"
  end
  
  def log_transmission(prescription, request, response)
    PrescriptionTransmission.create!(
      prescription: prescription,
      message_type: 'NewRx',
      message_version: '2017071',
      request_payload: request,
      response_payload: response.body,
      transmission_status: response.success? ? 'acknowledged' : 'failed',
      error_code: response.error_code,
      error_message: response.error_message,
      sent_at: Time.current
    )
  end
end

Drug Interaction Service

# app/services/drug_interaction_service.rb
class DrugInteractionService
  def self.call(patient:, new_medication:)
    current_medications = patient.medication_prescriptions.active
    
    interactions = []
    
    current_medications.each do |current_med|
      interaction = check_interaction(current_med, new_medication)
      interactions << interaction if interaction
    end
    
    InteractionResult.new(interactions)
  end
  
  private
  
  def self.check_interaction(med1, med2)
    # Check against drug interaction database
    # This would integrate with First Databank or similar
    
    # Mock implementation
    if med1.rxnorm_code == '855332' && med2.rxnorm_code == '392151'
      {
        severity: 'moderate',
        drug1: med1.medication_name,
        drug2: med2.name,
        description: 'May increase anticoagulant effect',
        recommendation: 'Monitor INR closely'
      }
    end
  end
  
  class InteractionResult
    attr_reader :interactions
    
    def initialize(interactions)
      @interactions = interactions
    end
    
    def severe
      interactions.select { |i| i[:severity] == 'severe' }
    end
    
    def moderate
      interactions.select { |i| i[:severity] == 'moderate' }
    end
    
    def any?
      interactions.any?
    end
  end
end

Security Implementation

Authentication & Authorization

# app/policies/prescription_policy.rb
class PrescriptionPolicy < ApplicationPolicy
  def create?
    user.has_role?(:doctor) || user.has_role?(:nurse_practitioner)
  end
  
  def send?
    create? && record.doctor_id == user.doctor_id
  end
  
  def cancel?
    send? && record.status.in?(['draft', 'sent'])
  end
  
  def view?
    user.clinic_id == record.clinic_id
  end
  
  def prescribe_controlled?
    create? && user.doctor.dea_number.present?
  end
end

# Two-Factor Authentication for Controlled Substances
class EPCSAuthenticationService
  def authenticate(user, prescription)
    return true unless prescription.controlled?
    
    # Verify two-factor authentication
    if user.two_factor_verified_at > 15.minutes.ago
      return true
    end
    
    # Require new two-factor authentication
    send_two_factor_code(user)
    false
  end
  
  def verify_two_factor(user, code)
    stored_code = Rails.cache.read("2fa_code_#{user.id}")
    
    if stored_code == code
      user.update!(two_factor_verified_at: Time.current)
      Rails.cache.delete("2fa_code_#{user.id}")
      true
    else
      false
    end
  end
  
  private
  
  def send_two_factor_code(user)
    code = SecureRandom.random_number(999999).to_s.rjust(6, '0')
    Rails.cache.write("2fa_code_#{user.id}", code, expires_in: 5.minutes)
    
    # Send via SMS or authenticator app
    TwilioService.send_sms(
      to: user.phone,
      body: "Your prescription verification code is: #{code}"
    )
  end
end

Audit Logging

# app/models/concerns/prescription_auditable.rb
module PrescriptionAuditable
  extend ActiveSupport::Concern
  
  included do
    after_create :log_creation
    after_update :log_update
    before_destroy :log_deletion
  end
  
  private
  
  def log_creation
    create_audit_log('create')
  end
  
  def log_update
    if significant_changes?
      create_audit_log('modify', changes: saved_changes)
    end
  end
  
  def log_deletion
    create_audit_log('delete')
  end
  
  def create_audit_log(action, changes: nil)
    PrescriptionAuditLog.create!(
      prescription_id: id,
      user_id: Current.user&.id,
      action: action,
      changes_made: changes,
      ip_address: Current.ip_address,
      user_agent: Current.user_agent,
      session_id: Current.session_id,
      is_controlled_substance: is_controlled?,
      created_at: Time.current
    )
  end
  
  def significant_changes?
    (saved_changes.keys & %w[
      medication_name dosage frequency quantity_prescribed
      pharmacy_id status refills_authorized
    ]).any?
  end
end

Error Handling & Fallback

Queue and Retry System

# app/jobs/prescription_transmission_job.rb
class PrescriptionTransmissionJob < ApplicationJob
  queue_as :prescriptions
  
  retry_on NetworkError, wait: :exponentially_longer, attempts: 5
  retry_on SurescriptsError, wait: 1.minute, attempts: 3
  
  def perform(prescription_id)
    prescription = ElectronicPrescription.find(prescription_id)
    
    return if prescription.status.in?(['sent', 'received', 'cancelled'])
    
    result = SurescriptsService.new.send_prescription(prescription)
    
    if result.success?
      notify_success(prescription)
    else
      handle_failure(prescription, result)
    end
  end
  
  private
  
  def handle_failure(prescription, result)
    if result.retryable?
      # Job will automatically retry
      prescription.update!(status: 'queued')
    else
      prescription.update!(
        status: 'failed',
        error_message: result.error_message
      )
      notify_failure(prescription)
    end
  end
  
  def notify_success(prescription)
    PrescriptionMailer.transmission_success(prescription).deliver_later
    
    if prescription.patient.notification_preferences.sms?
      TwilioService.send_sms(
        to: prescription.patient.phone,
        body: "Your prescription has been sent to #{prescription.pharmacy.name}"
      )
    end
  end
end
# app/services/prescription_print_service.rb
class PrescriptionPrintService
  def generate_pdf(prescription)
    Prawn::Document.new do |pdf|
      # Header with security features
      add_security_header(pdf, prescription)
      
      # Prescriber Information
      add_prescriber_section(pdf, prescription.doctor)
      
      # Patient Information
      add_patient_section(pdf, prescription.patient)
      
      # Prescription Details
      add_prescription_details(pdf, prescription)
      
      # Security Footer
      add_security_footer(pdf, prescription)
      
      # Add watermark for controlled substances
      add_controlled_watermark(pdf) if prescription.is_controlled?
    end
  end
  
  private
  
  def add_security_header(pdf, prescription)
    pdf.text "PRESCRIPTION - VOID IF ALTERED", size: 8, align: :center
    pdf.text "Security ID: #{prescription.prescription_guid}", size: 6
    pdf.move_down 10
  end
  
  def add_controlled_watermark(pdf)
    pdf.create_stamp("controlled") do
      pdf.rotate(45, origin: [0, 0]) do
        pdf.text "CONTROLLED SUBSTANCE", 
                 size: 60, 
                 color: "CCCCCC",
                 align: :center
      end
    end
    pdf.stamp_at("controlled", [100, 400])
  end
end

Performance Optimization

Caching Strategy

# config/initializers/cache_config.rb
Rails.application.configure do
  config.cache_store = :redis_cache_store, {
    url: ENV['REDIS_URL'],
    expires_in: 1.hour,
    namespace: 'eprescribe',
    pool_size: 5
  }
end

# app/services/drug_cache_service.rb
class DrugCacheService
  def self.search(query)
    cache_key = "drug_search:#{query.downcase}"
    
    Rails.cache.fetch(cache_key, expires_in: 24.hours) do
      DrugDatabase.where(
        "brand_name ILIKE :q OR generic_name ILIKE :q",
        q: "%#{query}%"
      ).limit(20).to_a
    end
  end
  
  def self.get_drug(rxnorm_code)
    Rails.cache.fetch("drug:#{rxnorm_code}", expires_in: 7.days) do
      DrugDatabase.find_by(rxnorm_code: rxnorm_code)
    end
  end
end

# app/services/pharmacy_cache_service.rb
class PharmacyCacheService
  def self.nearby(zip, radius_miles = 5)
    cache_key = "pharmacies:#{zip}:#{radius_miles}"
    
    Rails.cache.fetch(cache_key, expires_in: 1.day) do
      Pharmacy.near(zip, radius_miles).active.limit(50).to_a
    end
  end
end

Database Optimization

-- Partial indexes for common queries
CREATE INDEX idx_prescriptions_active 
  ON electronic_prescriptions(patient_id, status) 
  WHERE status IN ('draft', 'sent', 'queued');

CREATE INDEX idx_prescriptions_recent 
  ON electronic_prescriptions(doctor_id, created_at DESC) 
  WHERE created_at > CURRENT_DATE - INTERVAL '30 days';

-- Materialized view for prescription statistics
CREATE MATERIALIZED VIEW prescription_statistics AS
SELECT 
  doctor_id,
  DATE_TRUNC('day', created_at) as prescription_date,
  COUNT(*) as total_prescriptions,
  COUNT(*) FILTER (WHERE status = 'sent') as sent_count,
  COUNT(*) FILTER (WHERE status = 'failed') as failed_count,
  COUNT(*) FILTER (WHERE is_controlled = true) as controlled_count
FROM electronic_prescriptions
WHERE created_at > CURRENT_DATE - INTERVAL '90 days'
GROUP BY doctor_id, DATE_TRUNC('day', created_at);

-- Refresh daily
CREATE INDEX idx_prescription_stats ON prescription_statistics(doctor_id, prescription_date);

Monitoring & Alerts

Application Monitoring

# config/initializers/monitoring.rb
require 'prometheus/client'

prometheus = Prometheus::Client.registry

# Prescription metrics
PRESCRIPTION_COUNTER = Prometheus::Client::Counter.new(
  :prescriptions_total,
  docstring: 'Total number of prescriptions created',
  labels: [:status, :controlled]
)

PRESCRIPTION_TRANSMISSION_DURATION = Prometheus::Client::Histogram.new(
  :prescription_transmission_duration_seconds,
  docstring: 'Prescription transmission duration',
  buckets: [0.5, 1, 2, 5, 10]
)

DRUG_INTERACTION_CHECKS = Prometheus::Client::Counter.new(
  :drug_interaction_checks_total,
  docstring: 'Total drug interaction checks',
  labels: [:severity]
)

prometheus.register(PRESCRIPTION_COUNTER)
prometheus.register(PRESCRIPTION_TRANSMISSION_DURATION)
prometheus.register(DRUG_INTERACTION_CHECKS)

# Alert configurations
class PrescriptionAlerts
  def self.check_failure_rate
    recent_prescriptions = ElectronicPrescription.where(
      created_at: 1.hour.ago..Time.current
    )
    
    failure_rate = recent_prescriptions.failed.count.to_f / 
                   recent_prescriptions.count
    
    if failure_rate > 0.05 # 5% failure threshold
      AlertService.trigger(
        severity: 'high',
        message: "Prescription failure rate at #{(failure_rate * 100).round(2)}%"
      )
    end
  end
  
  def self.check_transmission_delays
    pending = PrescriptionTransmission.where(
      transmission_status: 'pending',
      created_at: ..30.minutes.ago
    ).count
    
    if pending > 10
      AlertService.trigger(
        severity: 'medium',
        message: "#{pending} prescriptions pending transmission > 30 minutes"
      )
    end
  end
end

Document Version: 1.0
Last Updated: November 2024
Author: Technical Architecture Team
Review Cycle: Sprint
API Version: v1

On this page