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
}
}Drug Search
# 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
endSurescripts 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
endDrug 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
endSecurity 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
endAudit 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
endError 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
endPrint Fallback
# 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
endPerformance 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
endDatabase 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
endDocument Version: 1.0
Last Updated: November 2024
Author: Technical Architecture Team
Review Cycle: Sprint
API Version: v1