agdar/core/consent_service.py
Marwan Alwali 2f1681b18c update
2025-11-11 13:44:48 +03:00

522 lines
17 KiB
Python

"""
Consent Management Service for comprehensive consent handling.
This module provides services for:
- Consent validity checking
- Auto consent status checks
- Consent version control
- Consent expiry alerts
"""
import logging
from datetime import date, timedelta
from typing import Dict, List, Optional, Tuple
from django.db.models import Q
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from core.models import Consent, ConsentTemplate, Patient, Tenant
logger = logging.getLogger(__name__)
class ConsentManagementService:
"""Service for managing consent lifecycle and validation."""
@staticmethod
def check_patient_consent_status(
patient: Patient,
consent_type: str = None
) -> Dict:
"""
Check consent status for a patient.
Args:
patient: Patient instance
consent_type: Optional specific consent type to check
Returns:
Dictionary with consent status information
"""
query = Consent.objects.filter(
patient=patient,
is_active=True
)
if consent_type:
query = query.filter(consent_type=consent_type)
consents = query.order_by('-created_at')
status = {
'has_valid_consent': False,
'expired_consents': [],
'expiring_soon': [],
'active_consents': [],
'missing_types': [],
}
# Check each consent type
required_types = [
Consent.ConsentType.GENERAL_TREATMENT,
Consent.ConsentType.SERVICE_SPECIFIC,
]
for req_type in required_types:
type_consents = consents.filter(consent_type=req_type)
if not type_consents.exists():
status['missing_types'].append(req_type)
continue
latest = type_consents.first()
if latest.is_expired:
status['expired_consents'].append({
'id': str(latest.id),
'type': latest.get_consent_type_display(),
'expiry_date': latest.expiry_date,
'days_expired': abs(latest.days_until_expiry) if latest.days_until_expiry else 0
})
elif latest.needs_renewal:
status['expiring_soon'].append({
'id': str(latest.id),
'type': latest.get_consent_type_display(),
'expiry_date': latest.expiry_date,
'days_remaining': latest.days_until_expiry
})
else:
status['active_consents'].append({
'id': str(latest.id),
'type': latest.get_consent_type_display(),
'expiry_date': latest.expiry_date,
'days_remaining': latest.days_until_expiry
})
# Patient has valid consent if all required types are active
status['has_valid_consent'] = (
len(status['missing_types']) == 0 and
len(status['expired_consents']) == 0
)
return status
@staticmethod
def get_expiring_consents(
tenant: Tenant,
days_threshold: int = 30
) -> List[Dict]:
"""
Get all consents expiring within threshold days.
Args:
tenant: Tenant instance
days_threshold: Number of days to look ahead
Returns:
List of expiring consent information
"""
threshold_date = date.today() + timedelta(days=days_threshold)
expiring_consents = Consent.objects.filter(
tenant=tenant,
is_active=True,
expiry_date__lte=threshold_date,
expiry_date__gte=date.today()
).select_related('patient').order_by('expiry_date')
result = []
for consent in expiring_consents:
result.append({
'consent_id': str(consent.id),
'patient_id': str(consent.patient.id),
'patient_name': consent.patient.full_name_en,
'patient_mrn': consent.patient.mrn,
'consent_type': consent.get_consent_type_display(),
'expiry_date': consent.expiry_date,
'days_remaining': consent.days_until_expiry,
'caregiver_phone': consent.patient.caregiver_phone,
'caregiver_email': consent.patient.email or consent.patient.caregiver_name,
})
return result
@staticmethod
def get_expired_consents(tenant: Tenant) -> List[Dict]:
"""
Get all expired consents.
Args:
tenant: Tenant instance
Returns:
List of expired consent information
"""
expired_consents = Consent.objects.filter(
tenant=tenant,
is_active=True,
expiry_date__lt=date.today()
).select_related('patient').order_by('expiry_date')
result = []
for consent in expired_consents:
result.append({
'consent_id': str(consent.id),
'patient_id': str(consent.patient.id),
'patient_name': consent.patient.full_name_en,
'patient_mrn': consent.patient.mrn,
'consent_type': consent.get_consent_type_display(),
'expiry_date': consent.expiry_date,
'days_expired': abs(consent.days_until_expiry) if consent.days_until_expiry else 0,
'caregiver_phone': consent.patient.caregiver_phone,
})
return result
@staticmethod
def create_consent_from_template(
patient: Patient,
template: ConsentTemplate,
expiry_days: int = 365,
language: str = 'en'
) -> Consent:
"""
Create a new consent from a template.
Args:
patient: Patient instance
template: ConsentTemplate instance
expiry_days: Number of days until expiry
language: Language for content ('en' or 'ar')
Returns:
Created Consent instance
"""
# Get populated content
content = template.get_populated_content(patient, language)
# Calculate expiry date
expiry_date = date.today() + timedelta(days=expiry_days)
# Create consent
consent = Consent.objects.create(
tenant=patient.tenant,
patient=patient,
consent_type=template.consent_type,
content_text=content,
version=template.version,
expiry_date=expiry_date,
is_active=True
)
logger.info(f"Created consent {consent.id} for patient {patient.mrn} from template {template.id}")
return consent
@staticmethod
def renew_consent(
old_consent: Consent,
expiry_days: int = 365
) -> Consent:
"""
Renew an expired or expiring consent.
Args:
old_consent: Existing consent to renew
expiry_days: Number of days until new expiry
Returns:
New Consent instance
"""
# Deactivate old consent
old_consent.is_active = False
old_consent.save()
# Create new consent with incremented version
new_consent = Consent.objects.create(
tenant=old_consent.tenant,
patient=old_consent.patient,
consent_type=old_consent.consent_type,
content_text=old_consent.content_text,
version=old_consent.version + 1,
expiry_date=date.today() + timedelta(days=expiry_days),
is_active=True
)
logger.info(f"Renewed consent {old_consent.id} with new consent {new_consent.id}")
return new_consent
@staticmethod
def get_active_template_version(
tenant: Tenant,
consent_type: str
) -> Optional[ConsentTemplate]:
"""
Get the active (latest) version of a consent template.
Args:
tenant: Tenant instance
consent_type: Type of consent
Returns:
Latest active ConsentTemplate or None
"""
return ConsentTemplate.objects.filter(
tenant=tenant,
consent_type=consent_type,
is_active=True
).order_by('-version').first()
@staticmethod
def create_new_template_version(
old_template: ConsentTemplate,
content_en: str,
content_ar: str = None
) -> ConsentTemplate:
"""
Create a new version of a consent template.
Args:
old_template: Existing template
content_en: New English content
content_ar: New Arabic content (optional)
Returns:
New ConsentTemplate instance
"""
# Deactivate old template
old_template.is_active = False
old_template.save()
# Create new version
new_template = ConsentTemplate.objects.create(
tenant=old_template.tenant,
consent_type=old_template.consent_type,
title_en=old_template.title_en,
title_ar=old_template.title_ar,
content_en=content_en,
content_ar=content_ar or old_template.content_ar,
version=old_template.version + 1,
is_active=True
)
logger.info(f"Created new template version {new_template.id} (v{new_template.version})")
return new_template
@staticmethod
def validate_consent_before_booking(
patient: Patient,
required_types: List[str] = None
) -> Tuple[bool, List[str]]:
"""
Validate that patient has all required active consents before booking.
Args:
patient: Patient instance
required_types: List of required consent types (defaults to GENERAL_TREATMENT)
Returns:
Tuple of (is_valid, list_of_missing_or_expired_types)
"""
if required_types is None:
required_types = [Consent.ConsentType.GENERAL_TREATMENT]
missing_or_expired = []
for consent_type in required_types:
# Get latest consent of this type
latest_consent = Consent.objects.filter(
patient=patient,
consent_type=consent_type,
is_active=True
).order_by('-created_at').first()
if not latest_consent:
missing_or_expired.append(f"{consent_type} (Missing)")
elif latest_consent.is_expired:
missing_or_expired.append(f"{consent_type} (Expired)")
is_valid = len(missing_or_expired) == 0
return is_valid, missing_or_expired
@staticmethod
def get_consent_statistics(tenant: Tenant) -> Dict:
"""
Get comprehensive consent statistics for a tenant.
Args:
tenant: Tenant instance
Returns:
Dictionary with consent statistics
"""
all_consents = Consent.objects.filter(tenant=tenant, is_active=True)
stats = {
'total_active': all_consents.count(),
'expiring_30_days': all_consents.filter(
expiry_date__lte=date.today() + timedelta(days=30),
expiry_date__gte=date.today()
).count(),
'expiring_7_days': all_consents.filter(
expiry_date__lte=date.today() + timedelta(days=7),
expiry_date__gte=date.today()
).count(),
'expired': all_consents.filter(
expiry_date__lt=date.today()
).count(),
'by_type': {},
'patients_without_consent': 0,
}
# Count by type
for consent_type, display_name in Consent.ConsentType.choices:
stats['by_type'][consent_type] = all_consents.filter(
consent_type=consent_type
).count()
# Count patients without any active consent
from core.models import Patient
total_patients = Patient.objects.filter(tenant=tenant).count()
patients_with_consent = all_consents.values('patient').distinct().count()
stats['patients_without_consent'] = total_patients - patients_with_consent
return stats
class ConsentNotificationService:
"""Service for sending consent-related notifications."""
@staticmethod
def send_expiry_reminder(consent: Consent) -> bool:
"""
Send expiry reminder to patient/caregiver.
Args:
consent: Consent instance
Returns:
True if notification sent successfully
"""
from core.tasks import send_email_task, create_notification_task
patient = consent.patient
days_remaining = consent.days_until_expiry
# Prepare message
message = _(
"Dear {caregiver_name},\n\n"
"The {consent_type} consent for {patient_name} (MRN: {mrn}) "
"will expire in {days} days on {expiry_date}.\n\n"
"Please contact the clinic to renew the consent.\n\n"
"Best regards,\nAgdar Centre"
).format(
caregiver_name=patient.caregiver_name or "Guardian",
consent_type=consent.get_consent_type_display(),
patient_name=patient.full_name_en,
mrn=patient.mrn,
days=days_remaining,
expiry_date=consent.expiry_date.strftime('%Y-%m-%d')
)
# Send email if available
if patient.email:
send_email_task.delay(
subject=f"Consent Expiry Reminder - {patient.mrn}",
message=message,
recipient_list=[patient.email]
)
# Send SMS if phone available
if patient.caregiver_phone:
# TODO: Integrate with SMS service
pass
logger.info(f"Sent expiry reminder for consent {consent.id}")
return True
@staticmethod
def send_expired_notification(consent: Consent) -> bool:
"""
Send notification that consent has expired.
Args:
consent: Consent instance
Returns:
True if notification sent successfully
"""
from core.tasks import send_email_task
patient = consent.patient
message = _(
"Dear {caregiver_name},\n\n"
"The {consent_type} consent for {patient_name} (MRN: {mrn}) "
"has expired as of {expiry_date}.\n\n"
"Please contact the clinic immediately to renew the consent. "
"No appointments can be booked until the consent is renewed.\n\n"
"Best regards,\nAgdar Centre"
).format(
caregiver_name=patient.caregiver_name or "Guardian",
consent_type=consent.get_consent_type_display(),
patient_name=patient.full_name_en,
mrn=patient.mrn,
expiry_date=consent.expiry_date.strftime('%Y-%m-%d')
)
# Send email if available
if patient.email:
send_email_task.delay(
subject=f"URGENT: Consent Expired - {patient.mrn}",
message=message,
recipient_list=[patient.email]
)
logger.info(f"Sent expired notification for consent {consent.id}")
return True
@staticmethod
def notify_reception_expired_consents(tenant: Tenant, expired_list: List[Dict]) -> bool:
"""
Notify reception staff about expired consents.
Args:
tenant: Tenant instance
expired_list: List of expired consent information
Returns:
True if notification sent successfully
"""
from core.tasks import create_notification_task
from core.models import User
# Get reception staff
reception_users = User.objects.filter(
tenant=tenant,
role=User.Role.FRONT_DESK,
is_active=True
)
if not expired_list:
return True
message = f"{len(expired_list)} patient consent(s) have expired and require renewal."
for user in reception_users:
create_notification_task.delay(
user_id=str(user.id),
title="Expired Consents Alert",
message=message,
notification_type='WARNING',
related_object_type='consent',
related_object_id=None
)
logger.info(f"Notified {reception_users.count()} reception staff about {len(expired_list)} expired consents")
return True