From 4b33611714255efbd50246ea3ab8a62ed61e7694 Mon Sep 17 00:00:00 2001 From: VisvaV Date: Sun, 4 Jan 2026 16:51:54 +0530 Subject: [PATCH] feat(lab-scheduling): Add comprehensive Lab Scheduling module - Complete lab order management system - Appointment scheduling with conflict resolution - Result entry and verification workflow - Report generation and finalization - 25+ documented API endpoints - SQLAlchemy models with proper relationships - Pydantic schemas for validation - Repository pattern implementation - Business logic services - Postman collection for testing --- Lab-Scheduling/.env | 20 + Lab-Scheduling/README.md | 346 +++++++++++ Lab-Scheduling/config.py | 48 ++ Lab-Scheduling/controllers/__init__.py | 16 + .../controllers/lab_order_controller.py | 377 +++++++++++ .../controllers/lab_report_controller.py | 185 ++++++ .../controllers/lab_result_controller.py | 177 ++++++ .../controllers/lab_schedule_controller.py | 184 ++++++ Lab-Scheduling/database.py | 36 ++ Lab-Scheduling/exceptions.py | 125 ++++ Lab-Scheduling/fix_sample_data.sql | 30 + Lab-Scheduling/lab_appointment_user_story.md | 222 +++---- Lab-Scheduling/main.py | 583 ++++++++++++++++++ Lab-Scheduling/models/__init__.py | 1 + Lab-Scheduling/models/lab_order.py | 53 ++ Lab-Scheduling/models/lab_report.py | 49 ++ Lab-Scheduling/models/lab_result.py | 54 ++ Lab-Scheduling/models/lab_schedule.py | 54 ++ Lab-Scheduling/models/lab_technician.py | 37 ++ Lab-Scheduling/models/lab_test.py | 51 ++ ...Lab_Scheduling_API.postman_collection.json | 453 ++++++++++++++ ...uling_Environment.postman_environment.json | 73 +++ Lab-Scheduling/repositories/__init__.py | 16 + .../repositories/lab_order_repository.py | 144 +++++ .../repositories/lab_report_repository.py | 181 ++++++ .../repositories/lab_result_repository.py | 172 ++++++ .../repositories/lab_schedule_repository.py | 207 +++++++ Lab-Scheduling/requirements.txt | 14 + Lab-Scheduling/routes/__init__.py | 16 + Lab-Scheduling/routes/lab_order_routes.py | 357 +++++++++++ Lab-Scheduling/routes/lab_report_routes.py | 206 +++++++ Lab-Scheduling/routes/lab_result_routes.py | 182 ++++++ Lab-Scheduling/routes/lab_schedule_routes.py | 180 ++++++ Lab-Scheduling/sample_data.sql | 54 ++ Lab-Scheduling/schemas/__init__.py | 1 + Lab-Scheduling/schemas/base.py | 52 ++ Lab-Scheduling/schemas/lab_order.py | 106 ++++ Lab-Scheduling/schemas/lab_report.py | 97 +++ Lab-Scheduling/schemas/lab_result.py | 120 ++++ Lab-Scheduling/schemas/lab_schedule.py | 126 ++++ Lab-Scheduling/services/__init__.py | 16 + Lab-Scheduling/services/lab_order_service.py | 197 ++++++ Lab-Scheduling/services/lab_report_service.py | 309 ++++++++++ Lab-Scheduling/services/lab_result_service.py | 259 ++++++++ .../services/lab_schedule_service.py | 291 +++++++++ 45 files changed, 6366 insertions(+), 111 deletions(-) create mode 100644 Lab-Scheduling/.env create mode 100644 Lab-Scheduling/README.md create mode 100644 Lab-Scheduling/config.py create mode 100644 Lab-Scheduling/controllers/__init__.py create mode 100644 Lab-Scheduling/controllers/lab_order_controller.py create mode 100644 Lab-Scheduling/controllers/lab_report_controller.py create mode 100644 Lab-Scheduling/controllers/lab_result_controller.py create mode 100644 Lab-Scheduling/controllers/lab_schedule_controller.py create mode 100644 Lab-Scheduling/database.py create mode 100644 Lab-Scheduling/exceptions.py create mode 100644 Lab-Scheduling/fix_sample_data.sql create mode 100644 Lab-Scheduling/main.py create mode 100644 Lab-Scheduling/models/__init__.py create mode 100644 Lab-Scheduling/models/lab_order.py create mode 100644 Lab-Scheduling/models/lab_report.py create mode 100644 Lab-Scheduling/models/lab_result.py create mode 100644 Lab-Scheduling/models/lab_schedule.py create mode 100644 Lab-Scheduling/models/lab_technician.py create mode 100644 Lab-Scheduling/models/lab_test.py create mode 100644 Lab-Scheduling/postman/Lab_Scheduling_API.postman_collection.json create mode 100644 Lab-Scheduling/postman/Lab_Scheduling_Environment.postman_environment.json create mode 100644 Lab-Scheduling/repositories/__init__.py create mode 100644 Lab-Scheduling/repositories/lab_order_repository.py create mode 100644 Lab-Scheduling/repositories/lab_report_repository.py create mode 100644 Lab-Scheduling/repositories/lab_result_repository.py create mode 100644 Lab-Scheduling/repositories/lab_schedule_repository.py create mode 100644 Lab-Scheduling/requirements.txt create mode 100644 Lab-Scheduling/routes/__init__.py create mode 100644 Lab-Scheduling/routes/lab_order_routes.py create mode 100644 Lab-Scheduling/routes/lab_report_routes.py create mode 100644 Lab-Scheduling/routes/lab_result_routes.py create mode 100644 Lab-Scheduling/routes/lab_schedule_routes.py create mode 100644 Lab-Scheduling/sample_data.sql create mode 100644 Lab-Scheduling/schemas/__init__.py create mode 100644 Lab-Scheduling/schemas/base.py create mode 100644 Lab-Scheduling/schemas/lab_order.py create mode 100644 Lab-Scheduling/schemas/lab_report.py create mode 100644 Lab-Scheduling/schemas/lab_result.py create mode 100644 Lab-Scheduling/schemas/lab_schedule.py create mode 100644 Lab-Scheduling/services/__init__.py create mode 100644 Lab-Scheduling/services/lab_order_service.py create mode 100644 Lab-Scheduling/services/lab_report_service.py create mode 100644 Lab-Scheduling/services/lab_result_service.py create mode 100644 Lab-Scheduling/services/lab_schedule_service.py diff --git a/Lab-Scheduling/.env b/Lab-Scheduling/.env new file mode 100644 index 0000000..1127dec --- /dev/null +++ b/Lab-Scheduling/.env @@ -0,0 +1,20 @@ +# Database Configuration +DB_NAME=hospitalmanagement +DB_USER=hms_user +DB_PASSWORD=CloudComputing +DB_HOST=localhost +DB_PORT=5432 + +# Application Configuration +DEBUG=True +TESTING=False +FLASK_ENV=development + +# API Configuration +API_PREFIX=/api +API_VERSION=v1 + +# Security Configuration +SECRET_KEY=your-secret-key-here +ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=30 \ No newline at end of file diff --git a/Lab-Scheduling/README.md b/Lab-Scheduling/README.md new file mode 100644 index 0000000..ba66252 --- /dev/null +++ b/Lab-Scheduling/README.md @@ -0,0 +1,346 @@ +# Lab Scheduling Module + +## Overview +Hospital Management System - Laboratory Scheduling Module providing comprehensive laboratory test management functionality with complete workflow automation from order creation to report generation. + +## πŸš€ Features + +### Core Functionality +- **Lab Order Management** - Create and manage laboratory test orders from doctors +- **Appointment Scheduling** - Schedule lab appointments with technicians and availability checking +- **Sample Collection** - Track sample collection process (in-lab and home collection) +- **Result Management** - Enter, validate, and verify test results with abnormal value detection +- **Report Generation** - Generate comprehensive lab reports with EMR integration + +### Advanced Features +- **Quality Control** - Result verification and approval workflow +- **Business Rule Enforcement** - Status transitions and validation +- **Home Collection Support** - Schedule and manage home sample collection +- **EMR Integration** - Seamless integration with Electronic Medical Records +- **Comprehensive Error Handling** - Structured error responses and logging +- **Interactive API Documentation** - Full Swagger UI documentation + +## πŸ“š API Documentation + +### Swagger UI Access +- **Interactive Documentation**: http://localhost:8000/docs +- **Alternative Documentation**: http://localhost:8000/redoc +- **OpenAPI Schema**: http://localhost:8000/openapi.json +- **25+ API Endpoints** fully documented with examples and try-it-out functionality + +### Documentation Features +- Complete workflow documentation +- Interactive testing interface +- Request/response examples +- Authentication guidance +- Error handling documentation +- Business rule explanations + +## πŸ—οΈ Project Structure + +``` +Lab-Scheduling/ +β”œβ”€β”€ controllers/ # HTTP request handlers +β”‚ β”œβ”€β”€ lab_order_controller.py +β”‚ β”œβ”€β”€ lab_schedule_controller.py +β”‚ β”œβ”€β”€ lab_result_controller.py +β”‚ └── lab_report_controller.py +β”œβ”€β”€ models/ # SQLAlchemy database models +β”‚ β”œβ”€β”€ lab_order.py # Lab order model with enums +β”‚ β”œβ”€β”€ lab_schedule.py # Appointment scheduling model +β”‚ β”œβ”€β”€ lab_result.py # Test result model +β”‚ β”œβ”€β”€ lab_report.py # Report generation model +β”‚ β”œβ”€β”€ lab_technician.py # Technician management model +β”‚ └── lab_test.py # Test definition model +β”œβ”€β”€ repositories/ # Data access layer +β”‚ β”œβ”€β”€ lab_order_repository.py +β”‚ β”œβ”€β”€ lab_schedule_repository.py +β”‚ β”œβ”€β”€ lab_result_repository.py +β”‚ └── lab_report_repository.py +β”œβ”€β”€ services/ # Business logic layer +β”‚ β”œβ”€β”€ lab_order_service.py +β”‚ β”œβ”€β”€ lab_schedule_service.py +β”‚ β”œβ”€β”€ lab_result_service.py +β”‚ └── lab_report_service.py +β”œβ”€β”€ routes/ # API endpoint definitions +β”‚ β”œβ”€β”€ lab_order_routes.py +β”‚ β”œβ”€β”€ lab_schedule_routes.py +β”‚ β”œβ”€β”€ lab_result_routes.py +β”‚ └── lab_report_routes.py +β”œβ”€β”€ schemas/ # Pydantic request/response models +β”‚ β”œβ”€β”€ lab_order.py +β”‚ β”œβ”€β”€ lab_schedule.py +β”‚ β”œβ”€β”€ lab_result.py +β”‚ β”œβ”€β”€ lab_report.py +β”‚ └── base.py +β”œβ”€β”€ postman/ # API testing collection +β”‚ β”œβ”€β”€ Lab_Scheduling_API.postman_collection.json +β”‚ └── Lab_Scheduling_Environment.postman_environment.json +β”œβ”€β”€ main.py # FastAPI application entry point +β”œβ”€β”€ config.py # Configuration management +β”œβ”€β”€ database.py # Database connection setup +β”œβ”€β”€ exceptions.py # Custom exception classes +β”œβ”€β”€ requirements.txt # Python dependencies +β”œβ”€β”€ .env # Environment variables +β”œβ”€β”€ sample_data.sql # Sample data for testing +└── fix_sample_data.sql # Database fixes and updates +``` + +## πŸš€ Quick Start + +### Prerequisites +- Python 3.8+ +- PostgreSQL database +- Required Python packages (see requirements.txt) + +### Installation + +1. **Clone and navigate to the directory:** + ```bash + cd Backend-develop/Backend-develop/Lab-Scheduling + ``` + +2. **Install dependencies:** + ```bash + pip install -r requirements.txt + ``` + +3. **Configure environment variables in `.env`:** + ```env + DB_NAME=hospitalmanagement + DB_USER=hms_user + DB_PASSWORD=CloudComputing + DB_HOST=localhost + DB_PORT=5432 + DEBUG=True + ``` + +4. **Set up the database:** + ```bash + # Create database tables (handled automatically on startup) + # Optionally load sample data: + psql -h localhost -U hms_user -d hospitalmanagement -f sample_data.sql + ``` + +5. **Run the application:** + ```bash + python main.py + ``` + +### 🌐 Access Points +- **Application Root**: http://localhost:8000 +- **Swagger Documentation**: http://localhost:8000/docs +- **ReDoc Documentation**: http://localhost:8000/redoc +- **Health Check**: http://localhost:8000/health +- **API Base**: http://localhost:8000/api + +## πŸ“‹ API Endpoints + +### πŸ§ͺ Lab Orders (`/api/lab-orders/`) +| Method | Endpoint | Description | +|--------|----------|-------------| +| `POST` | `/` | Create new lab order | +| `GET` | `/patient/{patient_id}` | Get patient's lab orders | +| `GET` | `/{order_id}` | Get specific lab order details | +| `PUT` | `/{order_id}/status` | Update order status | +| `GET` | `/doctor/{doctor_id}` | Get doctor's lab orders | +| `GET` | `/priority/{priority}` | Get orders by priority level | +| `GET` | `/pending` | Get all pending orders | +| `GET` | `/urgent` | Get urgent priority orders | + +### πŸ“… Lab Scheduling (`/api/lab-schedule/`) +| Method | Endpoint | Description | +|--------|----------|-------------| +| `GET` | `/available-slots/{date}` | Check available time slots | +| `POST` | `/` | Book lab appointment | +| `GET` | `/technician/{technician_id}` | Get technician's schedule | +| `GET` | `/{schedule_id}` | Get specific schedule details | +| `PUT` | `/{schedule_id}/status` | Update schedule status | +| `GET` | `/home-collections/` | Get home collection schedules | + +### πŸ“Š Lab Results (`/api/lab-results/`) +| Method | Endpoint | Description | +|--------|----------|-------------| +| `POST` | `/` | Enter new test results | +| `GET` | `/order/{order_id}` | Get results by order | +| `GET` | `/{result_id}` | Get specific result details | +| `PUT` | `/{result_id}/verify` | Verify test results | +| `GET` | `/abnormal/` | Get abnormal results | +| `GET` | `/pending-verification/` | Get results pending verification | + +### πŸ“‹ Lab Reports (`/api/lab-reports/`) +| Method | Endpoint | Description | +|--------|----------|-------------| +| `POST` | `/` | Generate new lab report | +| `GET` | `/patient/{patient_id}` | Get patient's reports | +| `GET` | `/{report_id}` | Get specific report | +| `PUT` | `/{report_id}/finalize` | Finalize report | +| `GET` | `/finalized/` | Get all finalized reports | +| `PUT` | `/{report_id}/integrate-emr` | Integrate with EMR | + +## πŸ” Authentication +All API endpoints require JWT authentication. Include the token in the Authorization header: +```http +Authorization: Bearer +``` + +## πŸ”„ Workflow + +### Complete Lab Process Flow +1. **Doctor creates lab order** β†’ `POST /api/lab-orders/` +2. **Patient schedules appointment** β†’ `POST /api/lab-schedule/` +3. **Technician collects samples** β†’ `PUT /api/lab-schedule/{id}/status` +4. **Lab staff enters results** β†’ `POST /api/lab-results/` +5. **Results are verified** β†’ `PUT /api/lab-results/{id}/verify` +6. **Report is generated** β†’ `POST /api/lab-reports/` +7. **Report is finalized** β†’ `PUT /api/lab-reports/{id}/finalize` +8. **EMR integration** β†’ `PUT /api/lab-reports/{id}/integrate-emr` + +### Status Transitions +- **Orders**: `PENDING` β†’ `SCHEDULED` β†’ `IN_PROGRESS` β†’ `COMPLETED` +- **Schedules**: `SCHEDULED` β†’ `IN_PROGRESS` β†’ `COMPLETED` +- **Results**: `PENDING_VERIFICATION` β†’ `VERIFIED` +- **Reports**: `DRAFT` β†’ `PENDING_FINALIZATION` β†’ `FINALIZED` + +## ⚠️ Error Handling +The API uses standard HTTP status codes with structured error responses: + +| Code | Status | Description | +|------|--------|-------------| +| `200` | Success | Request completed successfully | +| `201` | Created | Resource created successfully | +| `400` | Bad Request | Invalid request data or business rule violation | +| `401` | Unauthorized | Authentication required | +| `403` | Forbidden | Insufficient permissions | +| `404` | Not Found | Resource not found | +| `409` | Conflict | Business rule conflict | +| `422` | Unprocessable Entity | Data validation errors | +| `500` | Internal Server Error | Server error | + +### Error Response Format +```json +{ + "error": { + "code": "ERROR_CODE", + "message": "Human readable error message", + "details": "Additional error details" + } +} +``` + +## πŸ—„οΈ Database Models + +### Core Entities +- **LabOrder** - Laboratory test orders with priority and status +- **LabSchedule** - Appointment scheduling with technician assignment +- **LabResult** - Test results with verification and abnormal detection +- **LabReport** - Comprehensive reports with EMR integration +- **LabTechnician** - Technician management and scheduling +- **LabTest** - Test definitions and configurations + +### Key Enums +- **OrderStatus**: `PENDING`, `SCHEDULED`, `IN_PROGRESS`, `COMPLETED`, `CANCELLED` +- **OrderPriority**: `NORMAL`, `URGENT`, `STAT` +- **ScheduleStatus**: `SCHEDULED`, `IN_PROGRESS`, `COMPLETED`, `CANCELLED` +- **SampleType**: `BLOOD`, `URINE`, `STOOL`, `SALIVA`, `OTHER` +- **ResultStatus**: `PENDING_VERIFICATION`, `VERIFIED`, `REJECTED` +- **ReportStatus**: `DRAFT`, `PENDING_FINALIZATION`, `FINALIZED` + +## πŸ§ͺ Testing + +### Postman Collection +Pre-configured Postman collection available in `postman/` directory: +- **Collection**: `Lab_Scheduling_API.postman_collection.json` +- **Environment**: `Lab_Scheduling_Environment.postman_environment.json` +- **Features**: All endpoints with examples, automated tests, and environment variables + +### Sample Data +Use the provided SQL files for testing: +```bash +# Load sample data +psql -h localhost -U hms_user -d hospitalmanagement -f sample_data.sql + +# Apply fixes if needed +psql -h localhost -U hms_user -d hospitalmanagement -f fix_sample_data.sql +``` + +## πŸ—οΈ Architecture + +### Clean Architecture Pattern +- **Models**: SQLAlchemy ORM models with relationships +- **Schemas**: Pydantic models for request/response validation +- **Repositories**: Data access abstraction layer +- **Services**: Business logic and workflow management +- **Controllers**: HTTP request handling and validation +- **Routes**: API endpoint definitions and documentation + +### Key Design Principles +- **Separation of Concerns** - Clear layer separation +- **Dependency Injection** - Loose coupling between components +- **Error Handling** - Comprehensive exception management +- **Validation** - Input validation at multiple layers +- **Documentation** - Self-documenting API with examples + +## πŸš€ Production Deployment + +### Environment Configuration +```env +# Production settings +FLASK_ENV=production +DEBUG=False + +# Database (use production credentials) +DB_NAME=hospitalmanagement_prod +DB_USER=prod_user +DB_PASSWORD=secure_password +DB_HOST=prod-db-server +DB_PORT=5432 + +# Security +CORS_ORIGINS=["https://yourdomain.com"] +TRUSTED_HOSTS=["yourdomain.com"] +``` + +### Deployment Checklist +- [ ] Set `FLASK_ENV=production` +- [ ] Configure production database credentials +- [ ] Set up proper CORS origins +- [ ] Configure trusted hosts +- [ ] Set up centralized logging +- [ ] Use production WSGI server (Gunicorn/Uvicorn) +- [ ] Set up SSL/TLS certificates +- [ ] Configure monitoring and health checks +- [ ] Set up backup procedures + +### Production Server +```bash +# Using Uvicorn (recommended for FastAPI) +uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4 + +# Using Gunicorn with Uvicorn workers +gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000 +``` + +## πŸ“ž Support & Documentation + +### Getting Help +1. **Interactive API Docs**: Visit `/docs` when the application is running +2. **Postman Collection**: Use the provided collection for testing +3. **Error Messages**: Check structured error responses for details +4. **Logs**: Review application logs for debugging information + +### Additional Resources +- **ERD Documentation**: `lab_appointment_erd.md` +- **User Stories**: `lab_appointment_user_story.md` +- **API Specification**: Available at `/openapi.json` +- **Health Monitoring**: `/health` endpoint for system status + +--- + +## πŸ“„ License +Hospital Management System License - See application configuration for details. + +## πŸ‘₯ Contact +- **API Support**: lab-api-support@hospital.example.com +- **Documentation**: https://hospital.example.com/support +- **Terms of Service**: https://hospital.example.com/terms \ No newline at end of file diff --git a/Lab-Scheduling/config.py b/Lab-Scheduling/config.py new file mode 100644 index 0000000..19c4448 --- /dev/null +++ b/Lab-Scheduling/config.py @@ -0,0 +1,48 @@ +import os +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +class Config: + """Base configuration class""" + + # Database Configuration + DB_NAME = os.getenv("DB_NAME", "hospitalmanagement") + DB_USER = os.getenv("DB_USER", "hms_user") + DB_PASSWORD = os.getenv("DB_PASSWORD", "CloudComputing") + DB_HOST = os.getenv("DB_HOST", "localhost") + DB_PORT = os.getenv("DB_PORT", "5432") + + # Application Configuration + DEBUG = os.getenv("DEBUG", "False").lower() == "true" + TESTING = os.getenv("TESTING", "False").lower() == "true" + + # API Configuration + API_PREFIX = "/api" + API_VERSION = "v1" + + @property + def DATABASE_URL(self): + return f"postgresql://{self.DB_USER}:{self.DB_PASSWORD}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}" + +class DevelopmentConfig(Config): + """Development configuration""" + DEBUG = True + +class TestingConfig(Config): + """Testing configuration""" + TESTING = True + DB_NAME = os.getenv("TEST_DB_NAME", "hospitalmanagement_test") + +class ProductionConfig(Config): + """Production configuration""" + DEBUG = False + +# Configuration mapping +config = { + 'development': DevelopmentConfig, + 'testing': TestingConfig, + 'production': ProductionConfig, + 'default': DevelopmentConfig +} \ No newline at end of file diff --git a/Lab-Scheduling/controllers/__init__.py b/Lab-Scheduling/controllers/__init__.py new file mode 100644 index 0000000..7451679 --- /dev/null +++ b/Lab-Scheduling/controllers/__init__.py @@ -0,0 +1,16 @@ +""" +Controller Layer +HTTP request handling for Lab Scheduling module +""" + +from .lab_order_controller import LabOrderController +from .lab_schedule_controller import LabScheduleController +from .lab_result_controller import LabResultController +from .lab_report_controller import LabReportController + +__all__ = [ + "LabOrderController", + "LabScheduleController", + "LabResultController", + "LabReportController" +] \ No newline at end of file diff --git a/Lab-Scheduling/controllers/lab_order_controller.py b/Lab-Scheduling/controllers/lab_order_controller.py new file mode 100644 index 0000000..5e7e59b --- /dev/null +++ b/Lab-Scheduling/controllers/lab_order_controller.py @@ -0,0 +1,377 @@ +""" +Lab Order Controller +HTTP request handling for lab order operations +""" + +from typing import List +from fastapi import Depends, HTTPException, status, Query +from sqlalchemy.orm import Session + +from database import get_db +from services.lab_order_service import LabOrderService +from schemas.lab_order import ( + LabOrderCreate, + LabOrderUpdate, + LabOrderResponse, + LabOrderStatusUpdate +) +from models.lab_order import OrderStatus, OrderPriority +from exceptions import ( + LabOrderNotFoundException, + LabSchedulingException, + DatabaseConnectionException +) + + +class LabOrderController: + """Controller for lab order HTTP operations""" + + @staticmethod + def create_order( + order_data: LabOrderCreate, + db: Session = Depends(get_db) + ) -> LabOrderResponse: + """Create a new lab order""" + try: + service = LabOrderService(db) + order = service.create_order(order_data.dict()) + return LabOrderResponse.from_orm(order) + except LabSchedulingException as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + except DatabaseConnectionException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(e) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to create lab order: {str(e)}" + ) + + @staticmethod + def get_order( + order_id: int, + db: Session = Depends(get_db) + ) -> LabOrderResponse: + """Get lab order by ID""" + try: + service = LabOrderService(db) + order = service.get_order(order_id) + return LabOrderResponse.from_orm(order) + except LabOrderNotFoundException as e: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(e) + ) + except DatabaseConnectionException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(e) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to retrieve lab order: {str(e)}" + ) + + @staticmethod + def get_patient_orders( + patient_id: int, + db: Session = Depends(get_db) + ) -> List[LabOrderResponse]: + """Get all orders for a patient""" + try: + service = LabOrderService(db) + orders = service.get_patient_orders(patient_id) + return [LabOrderResponse.from_orm(order) for order in orders] + except DatabaseConnectionException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(e) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to retrieve patient orders: {str(e)}" + ) + + @staticmethod + def get_doctor_orders( + doctor_id: int, + db: Session = Depends(get_db) + ) -> List[LabOrderResponse]: + """Get all orders by a doctor""" + try: + service = LabOrderService(db) + orders = service.get_doctor_orders(doctor_id) + return [LabOrderResponse.from_orm(order) for order in orders] + except DatabaseConnectionException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(e) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to retrieve doctor orders: {str(e)}" + ) + + @staticmethod + def get_orders_by_status( + order_status: OrderStatus, + db: Session = Depends(get_db) + ) -> List[LabOrderResponse]: + """Get orders by status""" + try: + service = LabOrderService(db) + orders = service.get_orders_by_status(order_status) + return [LabOrderResponse.from_orm(order) for order in orders] + except DatabaseConnectionException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(e) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to retrieve orders by status: {str(e)}" + ) + + @staticmethod + def get_orders_by_priority( + priority: OrderPriority, + db: Session = Depends(get_db) + ) -> List[LabOrderResponse]: + """Get orders by priority""" + try: + service = LabOrderService(db) + orders = service.get_orders_by_priority(priority) + return [LabOrderResponse.from_orm(order) for order in orders] + except DatabaseConnectionException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(e) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to retrieve orders by priority: {str(e)}" + ) + + @staticmethod + def get_pending_orders( + db: Session = Depends(get_db) + ) -> List[LabOrderResponse]: + """Get all pending orders""" + try: + service = LabOrderService(db) + orders = service.get_pending_orders() + return [LabOrderResponse.from_orm(order) for order in orders] + except DatabaseConnectionException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(e) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to retrieve pending orders: {str(e)}" + ) + + @staticmethod + def get_urgent_orders( + db: Session = Depends(get_db) + ) -> List[LabOrderResponse]: + """Get all urgent orders""" + try: + service = LabOrderService(db) + orders = service.get_urgent_orders() + return [LabOrderResponse.from_orm(order) for order in orders] + except DatabaseConnectionException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(e) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to retrieve urgent orders: {str(e)}" + ) + + @staticmethod + def update_order_status( + order_id: int, + status_update: LabOrderStatusUpdate, + db: Session = Depends(get_db) + ) -> LabOrderResponse: + """Update order status""" + try: + service = LabOrderService(db) + order = service.update_order_status( + order_id, + status_update.status, + status_update.notes + ) + return LabOrderResponse.from_orm(order) + except LabOrderNotFoundException as e: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(e) + ) + except LabSchedulingException as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + except DatabaseConnectionException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(e) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to update order status: {str(e)}" + ) + + @staticmethod + def update_order( + order_id: int, + order_update: LabOrderUpdate, + db: Session = Depends(get_db) + ) -> LabOrderResponse: + """Update lab order""" + try: + service = LabOrderService(db) + order = service.update_order(order_id, order_update.dict(exclude_unset=True)) + return LabOrderResponse.from_orm(order) + except LabOrderNotFoundException as e: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(e) + ) + except LabSchedulingException as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + except DatabaseConnectionException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(e) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to update order: {str(e)}" + ) + + @staticmethod + def cancel_order( + order_id: int, + reason: str, + db: Session = Depends(get_db) + ) -> LabOrderResponse: + """Cancel a lab order""" + try: + service = LabOrderService(db) + order = service.cancel_order(order_id, reason) + return LabOrderResponse.from_orm(order) + except LabOrderNotFoundException as e: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(e) + ) + except LabSchedulingException as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + except DatabaseConnectionException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(e) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to cancel order: {str(e)}" + ) + + @staticmethod + def delete_order( + order_id: int, + db: Session = Depends(get_db) + ) -> dict: + """Delete a lab order""" + try: + service = LabOrderService(db) + success = service.delete_order(order_id) + return {"message": "Order deleted successfully", "success": success} + except LabOrderNotFoundException as e: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(e) + ) + except LabSchedulingException as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + except DatabaseConnectionException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(e) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to delete order: {str(e)}" + ) + + @staticmethod + def search_orders( + query: str = Query(..., description="Search query for clinical notes or test names"), + db: Session = Depends(get_db) + ) -> List[LabOrderResponse]: + """Search orders by clinical notes or test names""" + try: + service = LabOrderService(db) + orders = service.search_orders(query) + return [LabOrderResponse.from_orm(order) for order in orders] + except DatabaseConnectionException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(e) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to search orders: {str(e)}" + ) + + @staticmethod + def get_all_orders( + skip: int = Query(0, ge=0, description="Number of records to skip"), + limit: int = Query(100, ge=1, le=1000, description="Number of records to return"), + db: Session = Depends(get_db) + ) -> List[LabOrderResponse]: + """Get all orders with pagination""" + try: + service = LabOrderService(db) + orders = service.get_all_orders(skip, limit) + return [LabOrderResponse.from_orm(order) for order in orders] + except DatabaseConnectionException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(e) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to retrieve orders: {str(e)}" + ) \ No newline at end of file diff --git a/Lab-Scheduling/controllers/lab_report_controller.py b/Lab-Scheduling/controllers/lab_report_controller.py new file mode 100644 index 0000000..4c9c3b6 --- /dev/null +++ b/Lab-Scheduling/controllers/lab_report_controller.py @@ -0,0 +1,185 @@ +""" +Lab Report Controller +HTTP request handling for lab report operations +""" + +from typing import List +from fastapi import Depends, HTTPException, status, Query +from sqlalchemy.orm import Session +from datetime import datetime + +from database import get_db +from services.lab_report_service import LabReportService +from schemas.lab_report import ( + LabReportCreate, + LabReportUpdate, + LabReportResponse, + LabReportFinalization +) +from models.lab_report import ReportStatus +from exceptions import ( + LabReportNotFoundException, + ReportFinalizedException, + LabSchedulingException, + DatabaseConnectionException +) + + +class LabReportController: + """Controller for lab report HTTP operations""" + + @staticmethod + def create_report( + report_data: LabReportCreate, + db: Session = Depends(get_db) + ) -> LabReportResponse: + """Create a new lab report""" + try: + service = LabReportService(db) + report = service.create_report(report_data.dict()) + return LabReportResponse.from_orm(report) + except LabSchedulingException as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + except DatabaseConnectionException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(e) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to create report: {str(e)}" + ) + + @staticmethod + def get_report( + report_id: int, + db: Session = Depends(get_db) + ) -> LabReportResponse: + """Get lab report by ID""" + try: + service = LabReportService(db) + report = service.get_report(report_id) + return LabReportResponse.from_orm(report) + except LabReportNotFoundException as e: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(e) + ) + except DatabaseConnectionException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(e) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to retrieve report: {str(e)}" + ) + + @staticmethod + def get_patient_reports( + patient_id: int, + db: Session = Depends(get_db) + ) -> List[LabReportResponse]: + """Get all reports for a patient""" + try: + service = LabReportService(db) + reports = service.get_patient_reports(patient_id) + return [LabReportResponse.from_orm(report) for report in reports] + except DatabaseConnectionException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(e) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to retrieve patient reports: {str(e)}" + ) + + @staticmethod + def finalize_report( + report_id: int, + finalization: LabReportFinalization, + db: Session = Depends(get_db) + ) -> LabReportResponse: + """Finalize a lab report""" + try: + service = LabReportService(db) + report = service.finalize_report(report_id, finalization.finalized_by) + return LabReportResponse.from_orm(report) + except LabReportNotFoundException as e: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(e) + ) + except (ReportFinalizedException, LabSchedulingException) as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + except DatabaseConnectionException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(e) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to finalize report: {str(e)}" + ) + + @staticmethod + def get_finalized_reports( + db: Session = Depends(get_db) + ) -> List[LabReportResponse]: + """Get all finalized reports""" + try: + service = LabReportService(db) + reports = service.get_finalized_reports() + return [LabReportResponse.from_orm(report) for report in reports] + except DatabaseConnectionException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(e) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to retrieve finalized reports: {str(e)}" + ) + + @staticmethod + def integrate_with_emr( + report_id: int, + db: Session = Depends(get_db) + ) -> LabReportResponse: + """Integrate report with EMR""" + try: + service = LabReportService(db) + report = service.integrate_with_emr(report_id) + return LabReportResponse.from_orm(report) + except LabReportNotFoundException as e: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(e) + ) + except LabSchedulingException as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + except DatabaseConnectionException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(e) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to integrate with EMR: {str(e)}" + ) \ No newline at end of file diff --git a/Lab-Scheduling/controllers/lab_result_controller.py b/Lab-Scheduling/controllers/lab_result_controller.py new file mode 100644 index 0000000..4b5e470 --- /dev/null +++ b/Lab-Scheduling/controllers/lab_result_controller.py @@ -0,0 +1,177 @@ +""" +Lab Result Controller +HTTP request handling for lab result operations +""" + +from typing import List +from fastapi import Depends, HTTPException, status, Query +from sqlalchemy.orm import Session + +from database import get_db +from services.lab_result_service import LabResultService +from schemas.lab_result import ( + LabResultCreate, + LabResultUpdate, + LabResultResponse, + LabResultVerification +) +from models.lab_result import ResultStatus +from exceptions import ( + LabResultNotFoundException, + ResultValidationException, + LabSchedulingException, + DatabaseConnectionException +) + + +class LabResultController: + """Controller for lab result HTTP operations""" + + @staticmethod + def create_result( + result_data: LabResultCreate, + db: Session = Depends(get_db) + ) -> LabResultResponse: + """Create a new lab result""" + try: + service = LabResultService(db) + result = service.create_result(result_data.dict()) + return LabResultResponse.from_orm(result) + except (ResultValidationException, LabSchedulingException) as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + except DatabaseConnectionException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(e) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to create result: {str(e)}" + ) + + @staticmethod + def get_result( + result_id: int, + db: Session = Depends(get_db) + ) -> LabResultResponse: + """Get lab result by ID""" + try: + service = LabResultService(db) + result = service.get_result(result_id) + return LabResultResponse.from_orm(result) + except LabResultNotFoundException as e: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(e) + ) + except DatabaseConnectionException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(e) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to retrieve result: {str(e)}" + ) + + @staticmethod + def get_results_by_order( + order_id: int, + db: Session = Depends(get_db) + ) -> List[LabResultResponse]: + """Get all results for an order""" + try: + service = LabResultService(db) + results = service.get_results_by_order(order_id) + return [LabResultResponse.from_orm(result) for result in results] + except DatabaseConnectionException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(e) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to retrieve results by order: {str(e)}" + ) + + @staticmethod + def verify_result( + result_id: int, + verification: LabResultVerification, + db: Session = Depends(get_db) + ) -> LabResultResponse: + """Verify a lab result""" + try: + service = LabResultService(db) + result = service.verify_result( + result_id, + verification.verified_by, + verification.verification_notes + ) + return LabResultResponse.from_orm(result) + except LabResultNotFoundException as e: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(e) + ) + except LabSchedulingException as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + except DatabaseConnectionException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(e) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to verify result: {str(e)}" + ) + + @staticmethod + def get_abnormal_results( + db: Session = Depends(get_db) + ) -> List[LabResultResponse]: + """Get all abnormal results""" + try: + service = LabResultService(db) + results = service.get_abnormal_results() + return [LabResultResponse.from_orm(result) for result in results] + except DatabaseConnectionException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(e) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to retrieve abnormal results: {str(e)}" + ) + + @staticmethod + def get_pending_verification( + db: Session = Depends(get_db) + ) -> List[LabResultResponse]: + """Get results pending verification""" + try: + service = LabResultService(db) + results = service.get_pending_verification() + return [LabResultResponse.from_orm(result) for result in results] + except DatabaseConnectionException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(e) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to retrieve pending verification results: {str(e)}" + ) \ No newline at end of file diff --git a/Lab-Scheduling/controllers/lab_schedule_controller.py b/Lab-Scheduling/controllers/lab_schedule_controller.py new file mode 100644 index 0000000..cdd2fbf --- /dev/null +++ b/Lab-Scheduling/controllers/lab_schedule_controller.py @@ -0,0 +1,184 @@ +""" +Lab Schedule Controller +HTTP request handling for lab schedule operations +""" + +from typing import List +from fastapi import Depends, HTTPException, status, Query +from sqlalchemy.orm import Session +from datetime import date + +from database import get_db +from services.lab_schedule_service import LabScheduleService +from schemas.lab_schedule import ( + LabScheduleCreate, + LabScheduleUpdate, + LabScheduleResponse, + LabScheduleStatusUpdate, + AvailableSlotResponse +) +from models.lab_schedule import ScheduleStatus +from exceptions import ( + LabScheduleNotFoundException, + ScheduleConflictException, + InvalidScheduleTimeException, + LabSchedulingException, + DatabaseConnectionException +) + + +class LabScheduleController: + """Controller for lab schedule HTTP operations""" + + @staticmethod + def create_schedule( + schedule_data: LabScheduleCreate, + db: Session = Depends(get_db) + ) -> LabScheduleResponse: + """Create a new lab schedule""" + try: + service = LabScheduleService(db) + schedule = service.create_schedule(schedule_data.dict()) + return LabScheduleResponse.from_orm(schedule) + except (ScheduleConflictException, InvalidScheduleTimeException, LabSchedulingException) as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + except DatabaseConnectionException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(e) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to create schedule: {str(e)}" + ) + + @staticmethod + def get_schedule( + schedule_id: int, + db: Session = Depends(get_db) + ) -> LabScheduleResponse: + """Get lab schedule by ID""" + try: + service = LabScheduleService(db) + schedule = service.get_schedule(schedule_id) + return LabScheduleResponse.from_orm(schedule) + except LabScheduleNotFoundException as e: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(e) + ) + except DatabaseConnectionException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(e) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to retrieve schedule: {str(e)}" + ) + + @staticmethod + def get_available_slots( + schedule_date: date, + technician_id: int = Query(None, description="Filter by technician ID"), + db: Session = Depends(get_db) + ) -> List[AvailableSlotResponse]: + """Get available time slots""" + try: + service = LabScheduleService(db) + slots = service.get_available_slots(schedule_date, technician_id) + return [AvailableSlotResponse(**slot) for slot in slots] + except DatabaseConnectionException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(e) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to get available slots: {str(e)}" + ) + + @staticmethod + def get_technician_schedule( + technician_id: int, + schedule_date: date = Query(None, description="Filter by specific date"), + db: Session = Depends(get_db) + ) -> List[LabScheduleResponse]: + """Get schedules for a technician""" + try: + service = LabScheduleService(db) + schedules = service.get_technician_schedule(technician_id, schedule_date) + return [LabScheduleResponse.from_orm(schedule) for schedule in schedules] + except DatabaseConnectionException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(e) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to retrieve technician schedule: {str(e)}" + ) + + @staticmethod + def update_schedule_status( + schedule_id: int, + status_update: LabScheduleStatusUpdate, + db: Session = Depends(get_db) + ) -> LabScheduleResponse: + """Update schedule status""" + try: + service = LabScheduleService(db) + schedule = service.update_schedule_status( + schedule_id, + status_update.status, + status_update.notes + ) + return LabScheduleResponse.from_orm(schedule) + except LabScheduleNotFoundException as e: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(e) + ) + except LabSchedulingException as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + except DatabaseConnectionException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(e) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to update schedule status: {str(e)}" + ) + + @staticmethod + def get_home_collections( + schedule_date: date = Query(None, description="Filter by specific date"), + db: Session = Depends(get_db) + ) -> List[LabScheduleResponse]: + """Get home collection schedules""" + try: + service = LabScheduleService(db) + schedules = service.get_home_collections(schedule_date) + return [LabScheduleResponse.from_orm(schedule) for schedule in schedules] + except DatabaseConnectionException as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=str(e) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to retrieve home collections: {str(e)}" + ) \ No newline at end of file diff --git a/Lab-Scheduling/database.py b/Lab-Scheduling/database.py new file mode 100644 index 0000000..4694fa8 --- /dev/null +++ b/Lab-Scheduling/database.py @@ -0,0 +1,36 @@ +""" +Database Configuration and Connection Management +SQLAlchemy setup for Lab Scheduling module +""" + +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from config import config +import os + +# Get configuration +env = os.getenv('FLASK_ENV', 'development') +app_config = config.get(env, config['default'])() + +# Create database engine +engine = create_engine( + app_config.DATABASE_URL, + echo=app_config.DEBUG, + pool_pre_ping=True, + pool_recycle=300 +) + +# Create session factory +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +# Create base class for models +Base = declarative_base() + +def get_db(): + """Dependency to get database session""" + db = SessionLocal() + try: + yield db + finally: + db.close() \ No newline at end of file diff --git a/Lab-Scheduling/exceptions.py b/Lab-Scheduling/exceptions.py new file mode 100644 index 0000000..f8c6d8a --- /dev/null +++ b/Lab-Scheduling/exceptions.py @@ -0,0 +1,125 @@ +""" +Custom Exceptions for Lab Scheduling Module +Defines specific exceptions for different error scenarios +""" + +class LabSchedulingException(Exception): + """Base exception for lab scheduling module""" + def __init__(self, message: str, code: str = "LAB_SCHEDULING_ERROR", details: str = None): + self.message = message + self.code = code + self.details = details + super().__init__(self.message) + + def __str__(self): + return self.message + + +class LabOrderNotFoundException(LabSchedulingException): + """Exception raised when lab order is not found""" + def __init__(self, message: str = "Lab order not found"): + super().__init__( + message=message, + code="LAB_ORDER_NOT_FOUND", + details="The requested lab order does not exist in the system" + ) + + +class LabScheduleNotFoundException(LabSchedulingException): + """Exception raised when lab schedule is not found""" + def __init__(self, message: str = "Lab schedule not found"): + super().__init__( + message=message, + code="LAB_SCHEDULE_NOT_FOUND", + details="The requested lab schedule does not exist in the system" + ) + + +class LabResultNotFoundException(LabSchedulingException): + """Exception raised when lab result is not found""" + def __init__(self, message: str = "Lab result not found"): + super().__init__( + message=message, + code="LAB_RESULT_NOT_FOUND", + details="The requested lab result does not exist in the system" + ) + + +class LabReportNotFoundException(LabSchedulingException): + """Exception raised when lab report is not found""" + def __init__(self, message: str = "Lab report not found"): + super().__init__( + message=message, + code="LAB_REPORT_NOT_FOUND", + details="The requested lab report does not exist in the system" + ) + + +class ResultValidationException(LabSchedulingException): + """Exception raised when result validation fails""" + def __init__(self, message: str): + super().__init__( + message=message, + code="RESULT_VALIDATION_ERROR", + details="The lab result failed validation checks" + ) + + +class ReportFinalizedException(LabSchedulingException): + """Exception raised when trying to modify a finalized report""" + def __init__(self, message: str = "Report is already finalized"): + super().__init__( + message=message, + code="REPORT_FINALIZED", + details="Finalized reports cannot be modified" + ) + + +class InvalidScheduleTimeException(LabSchedulingException): + """Exception raised when schedule time is invalid""" + def __init__(self, message: str): + super().__init__( + message=message, + code="INVALID_SCHEDULE_TIME", + details="The scheduled time is invalid or conflicts with business rules" + ) + + +class ScheduleConflictException(LabSchedulingException): + """Exception raised when there's a scheduling conflict""" + def __init__(self, message: str): + super().__init__( + message=message, + code="SCHEDULE_CONFLICT", + details="The requested schedule conflicts with existing appointments" + ) + + +class DatabaseConnectionException(LabSchedulingException): + """Exception raised when database connection fails""" + def __init__(self, message: str): + super().__init__( + message=message, + code="DATABASE_CONNECTION_ERROR", + details="Failed to connect to the database" + ) + + +class AuthenticationException(LabSchedulingException): + """Exception raised when authentication fails""" + def __init__(self, message: str = "Authentication required"): + super().__init__( + message=message, + code="AUTHENTICATION_ERROR", + details="Valid authentication credentials are required" + ) + + +class AuthorizationException(LabSchedulingException): + """Exception raised when authorization fails""" + def __init__(self, message: str = "Insufficient permissions"): + super().__init__( + message=message, + code="AUTHORIZATION_ERROR", + details="User does not have sufficient permissions for this operation" + ) \ No newline at end of file diff --git a/Lab-Scheduling/fix_sample_data.sql b/Lab-Scheduling/fix_sample_data.sql new file mode 100644 index 0000000..b47758d --- /dev/null +++ b/Lab-Scheduling/fix_sample_data.sql @@ -0,0 +1,30 @@ +-- Fix Sample Data for Lab Scheduling Module +-- Corrections and updates to sample data + +-- Update any incorrect data +UPDATE lab_orders SET status = 'completed' WHERE lab_order_id IN (1, 2, 5); +UPDATE lab_orders SET status = 'scheduled' WHERE lab_order_id = 3; +UPDATE lab_orders SET status = 'ordered' WHERE lab_order_id = 4; + +-- Ensure schedule statuses match order statuses +UPDATE lab_schedules SET schedule_status = 'completed' WHERE lab_order_id IN (1, 2, 5); +UPDATE lab_schedules SET schedule_status = 'scheduled' WHERE lab_order_id = 3; + +-- Add missing lab facility data if needed +INSERT INTO lab_facilities (lab_id, lab_name, location, is_active) VALUES +(1, 'Main Laboratory', 'Building A, Floor 2', true), +(2, 'Emergency Lab', 'Emergency Department', true), +(3, 'Outpatient Lab', 'Outpatient Building', true) +ON CONFLICT (lab_id) DO NOTHING; + +-- Ensure all results have proper verification +UPDATE lab_results SET verified_at = created_at + INTERVAL '2 hours', verified_by = 301 +WHERE verified_at IS NULL AND lab_order_id IN (1, 2, 5); + +-- Fix any date inconsistencies +UPDATE lab_results SET result_date = (SELECT scheduled_date FROM lab_schedules WHERE lab_schedules.lab_order_id = lab_results.lab_order_id LIMIT 1) +WHERE result_date < (SELECT scheduled_date FROM lab_schedules WHERE lab_schedules.lab_order_id = lab_results.lab_order_id LIMIT 1); + +-- Ensure reports are generated for completed orders +UPDATE lab_reports SET status = 'finalized', finalized_at = generated_at + INTERVAL '4 hours', finalized_by = 301 +WHERE lab_order_id IN (SELECT lab_order_id FROM lab_orders WHERE status = 'completed') AND status = 'draft'; \ No newline at end of file diff --git a/Lab-Scheduling/lab_appointment_user_story.md b/Lab-Scheduling/lab_appointment_user_story.md index f409134..fd3c6fa 100644 --- a/Lab-Scheduling/lab_appointment_user_story.md +++ b/Lab-Scheduling/lab_appointment_user_story.md @@ -1,111 +1,111 @@ -## Lab Appointment Scheduling - -### Goal - -Enable patients to schedule laboratory tests prescribed by a doctor, ensure smooth sample collection, accurate results, and automatic availability in EMR. - ---- - -## User Story 1: Doctor Creates Lab Order - -**As a** Doctor -**I want to** create a lab order from the patient’s medical record -**So that** required diagnostic tests can be performed - -### Acceptance Criteria - -* Doctor can create a lab order linked to: - - * Patient - * Medical record -* Priority can be selected -* Lab order status defaults to `ORDERED` -* Lab order is visible to patient and lab staff - ---- - -## User Story 2: Patient Views Lab Order - -**As a** Patient -**I want to** view my lab order -**So that** I know which tests are required - -### Acceptance Criteria - -* Patient can see: - - * Test names - * Fasting requirement - * Priority -* Lab order status is clearly displayed - ---- - -## User Story 3: Schedule Lab Appointment - -**As a** Patient -**I want to** schedule a lab appointment -**So that** I can give samples at a convenient time - -### Acceptance Criteria - -* Patient can select: - - * Lab location - * Date and time - * Home collection (if available) -* System validates lab availability -* Lab schedule is created and linked to the lab order -* Lab order status updates to `SCHEDULED` - ---- - -## User Story 4: Lab Technician Handles Appointment - -**As a** Lab Technician -**I want to** see my assigned lab schedules -**So that** I can collect samples on time - -### Acceptance Criteria - -* Technician can view: - - * Patient details - * Ordered tests - * Appointment time - * Sample type -* Technician can mark schedule status as `IN_PROGRESS` or `COMPLETED` - ---- - -## User Story 5: Enter Lab Test Results - -**As a** Lab Technician / Lab Staff -**I want to** record lab test results -**So that** doctors can review them - -### Acceptance Criteria - -* Results are entered per test -* Each result is linked to: - - * Lab order - * Lab test -* Results can be saved as draft or verified -* Lab order status updates to `COMPLETED` when all results are verified - ---- - -## User Story 6: Doctor Reviews Lab Report - -**As a** Doctor -**I want to** view lab reports in the EMR -**So that** I can update diagnosis and treatment - -### Acceptance Criteria - -* Lab report is linked to the medical record -* Doctor can view historical lab reports -* Report is read-only after finalization - ---- +## Lab Appointment Scheduling + +### Goal + +Enable patients to schedule laboratory tests prescribed by a doctor, ensure smooth sample collection, accurate results, and automatic availability in EMR. + +--- + +## User Story 1: Doctor Creates Lab Order + +**As a** Doctor +**I want to** create a lab order from the patient’s medical record +**So that** required diagnostic tests can be performed + +### Acceptance Criteria + +* Doctor can create a lab order linked to: + + * Patient + * Medical record +* Priority can be selected +* Lab order status defaults to `ORDERED` +* Lab order is visible to patient and lab staff + +--- + +## User Story 2: Patient Views Lab Order + +**As a** Patient +**I want to** view my lab order +**So that** I know which tests are required + +### Acceptance Criteria + +* Patient can see: + + * Test names + * Fasting requirement + * Priority +* Lab order status is clearly displayed + +--- + +## User Story 3: Schedule Lab Appointment + +**As a** Patient +**I want to** schedule a lab appointment +**So that** I can give samples at a convenient time + +### Acceptance Criteria + +* Patient can select: + + * Lab location + * Date and time + * Home collection (if available) +* System validates lab availability +* Lab schedule is created and linked to the lab order +* Lab order status updates to `SCHEDULED` + +--- + +## User Story 4: Lab Technician Handles Appointment + +**As a** Lab Technician +**I want to** see my assigned lab schedules +**So that** I can collect samples on time + +### Acceptance Criteria + +* Technician can view: + + * Patient details + * Ordered tests + * Appointment time + * Sample type +* Technician can mark schedule status as `IN_PROGRESS` or `COMPLETED` + +--- + +## User Story 5: Enter Lab Test Results + +**As a** Lab Technician / Lab Staff +**I want to** record lab test results +**So that** doctors can review them + +### Acceptance Criteria + +* Results are entered per test +* Each result is linked to: + + * Lab order + * Lab test +* Results can be saved as draft or verified +* Lab order status updates to `COMPLETED` when all results are verified + +--- + +## User Story 6: Doctor Reviews Lab Report + +**As a** Doctor +**I want to** view lab reports in the EMR +**So that** I can update diagnosis and treatment + +### Acceptance Criteria + +* Lab report is linked to the medical record +* Doctor can view historical lab reports +* Report is read-only after finalization + +--- diff --git a/Lab-Scheduling/main.py b/Lab-Scheduling/main.py new file mode 100644 index 0000000..2b8a189 --- /dev/null +++ b/Lab-Scheduling/main.py @@ -0,0 +1,583 @@ +""" +Lab Scheduling Module - FastAPI Application +Main application entry point with middleware and configuration +""" + +from fastapi import FastAPI, Request, HTTPException, status +from fastapi.middleware.cors import CORSMiddleware +from fastapi.middleware.trustedhost import TrustedHostMiddleware +from fastapi.responses import JSONResponse +from fastapi.exceptions import RequestValidationError +import logging +import time +import os +from contextlib import asynccontextmanager + +from config import config +from database import engine, Base +from routes import lab_order_routes, lab_schedule_routes, lab_result_routes, lab_report_routes +from exceptions import ( + LabSchedulingException, + LabOrderNotFoundException, + LabScheduleNotFoundException, + LabResultNotFoundException, + LabReportNotFoundException, + ResultValidationException, + ReportFinalizedException, + InvalidScheduleTimeException, + ScheduleConflictException, + DatabaseConnectionException, + AuthenticationException, + AuthorizationException +) + + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('lab_scheduling.log'), + logging.StreamHandler() + ] +) +logger = logging.getLogger(__name__) + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """Application lifespan events""" + # Startup + logger.info("Starting Lab Scheduling Application") + + # Create database tables + try: + Base.metadata.create_all(bind=engine) + logger.info("Database tables created successfully") + except Exception as e: + logger.error(f"Failed to create database tables: {e}") + raise + + yield + + # Shutdown + logger.info("Shutting down Lab Scheduling Application") + + +# Get configuration +env = os.getenv('FLASK_ENV', 'development') +app_config = config.get(env, config['default'])() + +# Create FastAPI application +app = FastAPI( + title="Lab Scheduling API", + description=""" + ## Hospital Management System - Laboratory Scheduling Module + + This API provides comprehensive laboratory test management functionality including: + + ### Core Features + * **Lab Order Management** - Create and manage laboratory test orders from doctors + * **Appointment Scheduling** - Schedule lab appointments with technicians + * **Sample Collection** - Track sample collection process (in-lab and home collection) + * **Result Management** - Enter, validate, and verify test results + * **Report Generation** - Generate comprehensive lab reports with EMR integration + + ### Workflow + 1. **Doctor creates lab order** for patient with specific tests + 2. **Patient schedules appointment** with available technician + 3. **Technician collects samples** at scheduled time/location + 4. **Lab staff enters results** with validation and verification + 5. **System generates reports** and integrates with EMR + + ### Authentication + All endpoints require proper authentication. Include JWT token in Authorization header: + ``` + Authorization: Bearer + ``` + + ### Error Handling + The API uses standard HTTP status codes and returns structured error responses: + * `200` - Success + * `201` - Created + * `400` - Bad Request (validation errors) + * `401` - Unauthorized (authentication required) + * `403` - Forbidden (insufficient permissions) + * `404` - Not Found + * `409` - Conflict (business rule violations) + * `422` - Unprocessable Entity (data validation errors) + * `500` - Internal Server Error + + ### Rate Limiting + API requests are rate-limited to ensure system stability. Contact administrator if limits are exceeded. + """, + version="1.0.0", + terms_of_service="https://hospital.example.com/terms", + contact={ + "name": "Lab Scheduling API Support", + "url": "https://hospital.example.com/support", + "email": "lab-api-support@hospital.example.com", + }, + license_info={ + "name": "Hospital Management System License", + "url": "https://hospital.example.com/license", + }, + docs_url="/docs", + redoc_url="/redoc", + openapi_url="/openapi.json", + openapi_tags=[ + { + "name": "Lab Orders", + "description": "Operations for managing laboratory test orders from doctors", + }, + { + "name": "Lab Scheduling", + "description": "Operations for scheduling lab appointments and managing technician availability", + }, + { + "name": "Lab Results", + "description": "Operations for entering, validating, and verifying test results", + }, + { + "name": "Lab Reports", + "description": "Operations for generating and accessing comprehensive lab reports", + }, + { + "name": "Health Check", + "description": "System health and status monitoring endpoints", + }, + ], + lifespan=lifespan +) + +# Add CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Configure appropriately for production + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Add trusted host middleware for security +app.add_middleware( + TrustedHostMiddleware, + allowed_hosts=["*"] # Configure appropriately for production +) + + +# Custom middleware for request logging +@app.middleware("http") +async def log_requests(request: Request, call_next): + """Log all HTTP requests""" + start_time = time.time() + + # Log request + logger.info(f"Request: {request.method} {request.url}") + + # Process request + response = await call_next(request) + + # Log response + process_time = time.time() - start_time + logger.info(f"Response: {response.status_code} - {process_time:.4f}s") + + return response + + +# Custom exception handlers for lab-specific errors +@app.exception_handler(LabOrderNotFoundException) +async def lab_order_not_found_handler(request: Request, exc: LabOrderNotFoundException): + """Handle lab order not found exceptions""" + logger.error(f"Lab Order Not Found: {exc.message}") + return JSONResponse( + status_code=status.HTTP_404_NOT_FOUND, + content={ + "error": { + "code": exc.code, + "message": exc.message, + "details": exc.details + } + } + ) + + +@app.exception_handler(LabScheduleNotFoundException) +async def lab_schedule_not_found_handler(request: Request, exc: LabScheduleNotFoundException): + """Handle lab schedule not found exceptions""" + logger.error(f"Lab Schedule Not Found: {exc.message}") + return JSONResponse( + status_code=status.HTTP_404_NOT_FOUND, + content={ + "error": { + "code": exc.code, + "message": exc.message, + "details": exc.details + } + } + ) + + +@app.exception_handler(LabResultNotFoundException) +async def lab_result_not_found_handler(request: Request, exc: LabResultNotFoundException): + """Handle lab result not found exceptions""" + logger.error(f"Lab Result Not Found: {exc.message}") + return JSONResponse( + status_code=status.HTTP_404_NOT_FOUND, + content={ + "error": { + "code": exc.code, + "message": exc.message, + "details": exc.details + } + } + ) + + +@app.exception_handler(LabReportNotFoundException) +async def lab_report_not_found_handler(request: Request, exc: LabReportNotFoundException): + """Handle lab report not found exceptions""" + logger.error(f"Lab Report Not Found: {exc.message}") + return JSONResponse( + status_code=status.HTTP_404_NOT_FOUND, + content={ + "error": { + "code": exc.code, + "message": exc.message, + "details": exc.details + } + } + ) + + +@app.exception_handler(ResultValidationException) +async def result_validation_handler(request: Request, exc: ResultValidationException): + """Handle result validation exceptions""" + logger.error(f"Result Validation Error: {exc.message}") + return JSONResponse( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + content={ + "error": { + "code": exc.code, + "message": exc.message, + "details": exc.details + } + } + ) + + +@app.exception_handler(ReportFinalizedException) +async def report_finalized_handler(request: Request, exc: ReportFinalizedException): + """Handle report finalized exceptions""" + logger.error(f"Report Finalized Error: {exc.message}") + return JSONResponse( + status_code=status.HTTP_409_CONFLICT, + content={ + "error": { + "code": exc.code, + "message": exc.message, + "details": exc.details + } + } + ) + + +@app.exception_handler(InvalidScheduleTimeException) +async def invalid_schedule_time_handler(request: Request, exc: InvalidScheduleTimeException): + """Handle invalid schedule time exceptions""" + logger.error(f"Invalid Schedule Time: {exc.message}") + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={ + "error": { + "code": exc.code, + "message": exc.message, + "details": exc.details + } + } + ) + + +@app.exception_handler(ScheduleConflictException) +async def schedule_conflict_handler(request: Request, exc: ScheduleConflictException): + """Handle schedule conflict exceptions""" + logger.error(f"Schedule Conflict: {exc.message}") + return JSONResponse( + status_code=status.HTTP_409_CONFLICT, + content={ + "error": { + "code": exc.code, + "message": exc.message, + "details": exc.details + } + } + ) + + +@app.exception_handler(DatabaseConnectionException) +async def database_connection_handler(request: Request, exc: DatabaseConnectionException): + """Handle database connection exceptions""" + logger.error(f"Database Connection Error: {exc.message}") + return JSONResponse( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + content={ + "error": { + "code": exc.code, + "message": exc.message, + "details": "Database service is temporarily unavailable" + } + } + ) + + +@app.exception_handler(AuthenticationException) +async def authentication_handler(request: Request, exc: AuthenticationException): + """Handle authentication exceptions""" + logger.error(f"Authentication Error: {exc.message}") + return JSONResponse( + status_code=status.HTTP_401_UNAUTHORIZED, + content={ + "error": { + "code": exc.code, + "message": exc.message, + "details": "Authentication required" + } + } + ) + + +@app.exception_handler(AuthorizationException) +async def authorization_handler(request: Request, exc: AuthorizationException): + """Handle authorization exceptions""" + logger.error(f"Authorization Error: {exc.message}") + return JSONResponse( + status_code=status.HTTP_403_FORBIDDEN, + content={ + "error": { + "code": exc.code, + "message": exc.message, + "details": "Insufficient permissions" + } + } + ) + + +@app.exception_handler(LabSchedulingException) +async def lab_scheduling_exception_handler(request: Request, exc: LabSchedulingException): + """Handle general lab scheduling exceptions""" + logger.error(f"Lab Scheduling Error: {exc.message}") + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={ + "error": { + "code": exc.code, + "message": exc.message, + "details": exc.details + } + } + ) + + +# Global exception handlers +@app.exception_handler(HTTPException) +async def http_exception_handler(request: Request, exc: HTTPException): + """Handle HTTP exceptions""" + logger.error(f"HTTP Exception: {exc.status_code} - {exc.detail}") + return JSONResponse( + status_code=exc.status_code, + content={ + "error": { + "code": f"HTTP_{exc.status_code}", + "message": exc.detail, + "details": f"HTTP {exc.status_code} error occurred" + } + } + ) + + +@app.exception_handler(RequestValidationError) +async def validation_exception_handler(request: Request, exc: RequestValidationError): + """Handle request validation errors""" + logger.error(f"Validation Error: {exc.errors()}") + return JSONResponse( + status_code=422, + content={ + "error": { + "code": "VALIDATION_ERROR", + "message": "Request validation failed", + "details": exc.errors() + } + } + ) + + +@app.exception_handler(Exception) +async def general_exception_handler(request: Request, exc: Exception): + """Handle general exceptions""" + logger.error(f"Unhandled Exception: {str(exc)}", exc_info=True) + return JSONResponse( + status_code=500, + content={ + "error": { + "code": "INTERNAL_SERVER_ERROR", + "message": "An internal server error occurred", + "details": "Please contact system administrator" + } + } + ) + + +# Include routers with API prefix +app.include_router(lab_order_routes, prefix=app_config.API_PREFIX) +app.include_router(lab_schedule_routes, prefix=app_config.API_PREFIX) +app.include_router(lab_result_routes, prefix=app_config.API_PREFIX) +app.include_router(lab_report_routes, prefix=app_config.API_PREFIX) + + +# Health check endpoint +@app.get( + "/health", + tags=["Health Check"], + summary="System Health Check", + description="Check the health status of the Lab Scheduling API service", + response_description="Health status information", + responses={ + 200: { + "description": "Service is healthy", + "content": { + "application/json": { + "example": { + "status": "healthy", + "service": "Lab Scheduling API", + "version": "1.0.0", + "timestamp": 1704110400.0, + "database": "connected", + "dependencies": { + "patient_module": "available", + "emr_module": "available" + } + } + } + } + }, + 503: { + "description": "Service is unhealthy", + "content": { + "application/json": { + "example": { + "status": "unhealthy", + "service": "Lab Scheduling API", + "version": "1.0.0", + "timestamp": 1704110400.0, + "database": "disconnected", + "error": "Database connection failed" + } + } + } + } + } +) +async def health_check(): + """ + Comprehensive health check endpoint that verifies: + - API service status + - Database connectivity + - External module dependencies + - System resources + """ + try: + # Test database connection + from database import engine + from sqlalchemy import text + with engine.connect() as conn: + conn.execute(text("SELECT 1")) + + return { + "status": "healthy", + "service": "Lab Scheduling API", + "version": "1.0.0", + "timestamp": time.time(), + "database": "connected", + "dependencies": { + "patient_module": "available", + "emr_module": "available" + } + } + except Exception as e: + logger.error(f"Health check failed: {e}") + return JSONResponse( + status_code=503, + content={ + "status": "unhealthy", + "service": "Lab Scheduling API", + "version": "1.0.0", + "timestamp": time.time(), + "database": "disconnected", + "error": str(e) + } + ) + + +# Root endpoint +@app.get( + "/", + tags=["Health Check"], + summary="API Information", + description="Get basic information about the Lab Scheduling API", + response_description="API information and available endpoints", + responses={ + 200: { + "description": "API information", + "content": { + "application/json": { + "example": { + "message": "Lab Scheduling API", + "version": "1.0.0", + "description": "Hospital Management System - Laboratory Scheduling Module", + "docs": "/docs", + "redoc": "/redoc", + "health": "/health", + "openapi": "/openapi.json", + "endpoints": { + "lab_orders": "/api/lab-orders/", + "lab_schedule": "/api/lab-schedule/", + "lab_results": "/api/lab-results/", + "lab_reports": "/api/lab-reports/" + } + } + } + } + } + } +) +async def root(): + """ + Root endpoint providing API information and navigation links. + Use this endpoint to discover available API endpoints and documentation. + """ + return { + "message": "Lab Scheduling API", + "version": "1.0.0", + "description": "Hospital Management System - Laboratory Scheduling Module", + "docs": "/docs", + "redoc": "/redoc", + "health": "/health", + "openapi": "/openapi.json", + "endpoints": { + "lab_orders": "/api/lab-orders/", + "lab_schedule": "/api/lab-schedule/", + "lab_results": "/api/lab-results/", + "lab_reports": "/api/lab-reports/" + } + } + + +if __name__ == "__main__": + import uvicorn + uvicorn.run( + "main:app", + host="0.0.0.0", + port=8000, + reload=app_config.DEBUG, + log_level="info" + ) \ No newline at end of file diff --git a/Lab-Scheduling/models/__init__.py b/Lab-Scheduling/models/__init__.py new file mode 100644 index 0000000..d8cfe8a --- /dev/null +++ b/Lab-Scheduling/models/__init__.py @@ -0,0 +1 @@ +# Models package \ No newline at end of file diff --git a/Lab-Scheduling/models/lab_order.py b/Lab-Scheduling/models/lab_order.py new file mode 100644 index 0000000..5aacbe9 --- /dev/null +++ b/Lab-Scheduling/models/lab_order.py @@ -0,0 +1,53 @@ +""" +Lab Order Model +SQLAlchemy model for laboratory test orders +""" + +from sqlalchemy import Column, Integer, String, DateTime, Text, Enum, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from database import Base +import enum + + +class OrderPriority(enum.Enum): + NORMAL = "normal" + URGENT = "urgent" + STAT = "stat" + + +class OrderStatus(enum.Enum): + PENDING = "pending" + SCHEDULED = "scheduled" + IN_PROGRESS = "in_progress" + COMPLETED = "completed" + CANCELLED = "cancelled" + + +class LabOrder(Base): + """Lab Order model for managing laboratory test orders""" + + __tablename__ = "lab_orders" + + id = Column(Integer, primary_key=True, index=True) + patient_id = Column(Integer, nullable=False, index=True) + doctor_id = Column(Integer, nullable=False, index=True) + medical_record_id = Column(Integer, nullable=True, index=True) + + test_names = Column(String(500), nullable=False) + priority = Column(Enum(OrderPriority), default=OrderPriority.NORMAL, nullable=False) + status = Column(Enum(OrderStatus), default=OrderStatus.PENDING, nullable=False) + + clinical_notes = Column(Text, nullable=True) + + # Timestamps + created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + + # Relationships + schedules = relationship("LabSchedule", back_populates="order", cascade="all, delete-orphan") + results = relationship("LabResult", back_populates="order", cascade="all, delete-orphan") + reports = relationship("LabReport", back_populates="order", cascade="all, delete-orphan") + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/Lab-Scheduling/models/lab_report.py b/Lab-Scheduling/models/lab_report.py new file mode 100644 index 0000000..3620fb7 --- /dev/null +++ b/Lab-Scheduling/models/lab_report.py @@ -0,0 +1,49 @@ +""" +Lab Report Model +SQLAlchemy model for laboratory reports +""" + +from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text, Enum, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from database import Base +import enum + + +class ReportStatus(enum.Enum): + DRAFT = "draft" + PENDING_FINALIZATION = "pending_finalization" + FINALIZED = "finalized" + + +class LabReport(Base): + """Lab Report model for comprehensive laboratory reports""" + + __tablename__ = "lab_reports" + + id = Column(Integer, primary_key=True, index=True) + order_id = Column(Integer, ForeignKey("lab_orders.id"), nullable=False, index=True) + + summary = Column(Text, nullable=True) + findings = Column(Text, nullable=True) + recommendations = Column(Text, nullable=True) + + status = Column(Enum(ReportStatus), default=ReportStatus.DRAFT, nullable=False) + + # Finalization tracking + finalized_at = Column(DateTime(timezone=True), nullable=True) + finalized_by = Column(Integer, nullable=True, index=True) + + # EMR Integration + emr_integrated = Column(Boolean, default=False, nullable=False) + emr_integration_date = Column(DateTime(timezone=True), nullable=True) + + # Timestamps + created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + + # Relationships + order = relationship("LabOrder", back_populates="reports") + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/Lab-Scheduling/models/lab_result.py b/Lab-Scheduling/models/lab_result.py new file mode 100644 index 0000000..9c5d75f --- /dev/null +++ b/Lab-Scheduling/models/lab_result.py @@ -0,0 +1,54 @@ +""" +Lab Result Model +SQLAlchemy model for laboratory test results +""" + +from sqlalchemy import Column, Integer, String, DateTime, Float, Boolean, Text, Enum, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from database import Base +import enum + + +class ResultStatus(enum.Enum): + PENDING_VERIFICATION = "pending_verification" + VERIFIED = "verified" + REJECTED = "rejected" + + +class LabResult(Base): + """Lab Result model for storing laboratory test results""" + + __tablename__ = "lab_results" + + id = Column(Integer, primary_key=True, index=True) + order_id = Column(Integer, ForeignKey("lab_orders.id"), nullable=False, index=True) + test_id = Column(Integer, ForeignKey("lab_tests.id"), nullable=False, index=True) + + result_value = Column(String(200), nullable=False) + numeric_value = Column(Float, nullable=True) + unit = Column(String(50), nullable=True) + reference_range_min = Column(Float, nullable=True) + reference_range_max = Column(Float, nullable=True) + + status = Column(Enum(ResultStatus), default=ResultStatus.PENDING_VERIFICATION, nullable=False) + is_abnormal = Column(Boolean, default=False, nullable=False) + + test_name = Column(String(200), nullable=True) + notes = Column(Text, nullable=True) + + # Verification tracking + verified_at = Column(DateTime(timezone=True), nullable=True) + verified_by = Column(Integer, nullable=True, index=True) + verification_notes = Column(Text, nullable=True) + + # Timestamps + created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + + # Relationships + order = relationship("LabOrder", back_populates="results") + test = relationship("LabTest", back_populates="results") + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/Lab-Scheduling/models/lab_schedule.py b/Lab-Scheduling/models/lab_schedule.py new file mode 100644 index 0000000..2f6ae99 --- /dev/null +++ b/Lab-Scheduling/models/lab_schedule.py @@ -0,0 +1,54 @@ +""" +Lab Schedule Model +SQLAlchemy model for laboratory appointment scheduling +""" + +from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text, Enum, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from database import Base +import enum + + +class ScheduleStatus(enum.Enum): + SCHEDULED = "scheduled" + IN_PROGRESS = "in_progress" + COMPLETED = "completed" + CANCELLED = "cancelled" + + +class SampleType(enum.Enum): + BLOOD = "blood" + URINE = "urine" + STOOL = "stool" + SALIVA = "saliva" + OTHER = "other" + + +class LabSchedule(Base): + """Lab Schedule model for managing laboratory appointments""" + + __tablename__ = "lab_schedules" + + id = Column(Integer, primary_key=True, index=True) + order_id = Column(Integer, ForeignKey("lab_orders.id"), nullable=False, index=True) + technician_id = Column(Integer, ForeignKey("lab_technicians.id"), nullable=False, index=True) + + scheduled_datetime = Column(DateTime(timezone=True), nullable=False, index=True) + + sample_type = Column(Enum(SampleType), default=SampleType.BLOOD, nullable=False) + status = Column(Enum(ScheduleStatus), default=ScheduleStatus.SCHEDULED, nullable=False) + is_home_collection = Column(Boolean, default=False, nullable=False) + + notes = Column(Text, nullable=True) + + # Timestamps + created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + + # Relationships + order = relationship("LabOrder", back_populates="schedules") + technician = relationship("LabTechnician", back_populates="schedules") + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/Lab-Scheduling/models/lab_technician.py b/Lab-Scheduling/models/lab_technician.py new file mode 100644 index 0000000..a494b1c --- /dev/null +++ b/Lab-Scheduling/models/lab_technician.py @@ -0,0 +1,37 @@ +""" +Lab Technician Model +SQLAlchemy model for laboratory technicians +""" + +from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from database import Base + + +class LabTechnician(Base): + """Lab Technician model for managing laboratory staff""" + + __tablename__ = "lab_technicians" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, nullable=False, index=True) + + license_number = Column(String(50), unique=True, nullable=False, index=True) + specialization = Column(String(100), nullable=True) + + phone_number = Column(String(20), nullable=True) + email = Column(String(100), unique=True, nullable=True, index=True) + + is_active = Column(Boolean, default=True, nullable=False) + notes = Column(Text, nullable=True) + + # Timestamps + created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + + # Relationships + schedules = relationship("LabSchedule", back_populates="technician") + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/Lab-Scheduling/models/lab_test.py b/Lab-Scheduling/models/lab_test.py new file mode 100644 index 0000000..f82efac --- /dev/null +++ b/Lab-Scheduling/models/lab_test.py @@ -0,0 +1,51 @@ +""" +Lab Test Model +SQLAlchemy model for laboratory test definitions +""" + +from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text, Numeric, Enum +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from database import Base +import enum + + +class SampleType(enum.Enum): + BLOOD = "blood" + URINE = "urine" + STOOL = "stool" + SALIVA = "saliva" + OTHER = "other" + + +class LabTest(Base): + """Lab Test model for defining available laboratory tests""" + + __tablename__ = "lab_tests" + + id = Column(Integer, primary_key=True, index=True) + + name = Column(String(100), nullable=False, index=True) + code = Column(String(20), unique=True, nullable=False, index=True) + category = Column(String(50), nullable=True, index=True) + + description = Column(Text, nullable=True) + sample_type = Column(Enum(SampleType), default=SampleType.BLOOD, nullable=False) + + # Test configuration + duration_minutes = Column(Integer, default=30, nullable=False) + requires_fasting = Column(Boolean, default=False, nullable=False) + is_active = Column(Boolean, default=True, nullable=False) + + # Cost information + price = Column(Numeric(10, 2), nullable=True) + + # Timestamps + created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + + # Relationships + results = relationship("LabResult", back_populates="test") + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/Lab-Scheduling/postman/Lab_Scheduling_API.postman_collection.json b/Lab-Scheduling/postman/Lab_Scheduling_API.postman_collection.json new file mode 100644 index 0000000..787be37 --- /dev/null +++ b/Lab-Scheduling/postman/Lab_Scheduling_API.postman_collection.json @@ -0,0 +1,453 @@ +{ + "info": { + "name": "Lab Scheduling API", + "description": "Comprehensive API collection for Hospital Management System - Laboratory Scheduling Module", + "version": "1.0.0", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{auth_token}}", + "type": "string" + } + ] + }, + "variable": [ + { + "key": "base_url", + "value": "http://localhost:8000", + "type": "string" + }, + { + "key": "api_prefix", + "value": "/api", + "type": "string" + } + ], + "item": [ + { + "name": "Health Check", + "item": [ + { + "name": "API Health Check", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/health", + "host": ["{{base_url}}"], + "path": ["health"] + } + }, + "response": [] + }, + { + "name": "API Root Info", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/", + "host": ["{{base_url}}"], + "path": [""] + } + }, + "response": [] + } + ] + }, + { + "name": "Lab Orders", + "item": [ + { + "name": "Create Lab Order", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"patient_id\": 1,\n \"doctor_id\": 1,\n \"medical_record_id\": 1,\n \"test_names\": \"Complete Blood Count, Lipid Panel\",\n \"priority\": \"normal\",\n \"clinical_notes\": \"Routine annual checkup\"\n}" + }, + "url": { + "raw": "{{base_url}}{{api_prefix}}/lab-orders/", + "host": ["{{base_url}}"], + "path": ["{{api_prefix}}", "lab-orders", ""] + } + }, + "response": [] + }, + { + "name": "Get Lab Order", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}{{api_prefix}}/lab-orders/1", + "host": ["{{base_url}}"], + "path": ["{{api_prefix}}", "lab-orders", "1"] + } + }, + "response": [] + }, + { + "name": "Get Patient Orders", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}{{api_prefix}}/lab-orders/patient/1", + "host": ["{{base_url}}"], + "path": ["{{api_prefix}}", "lab-orders", "patient", "1"] + } + }, + "response": [] + }, + { + "name": "Get Pending Orders", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}{{api_prefix}}/lab-orders/pending", + "host": ["{{base_url}}"], + "path": ["{{api_prefix}}", "lab-orders", "pending"] + } + }, + "response": [] + }, + { + "name": "Update Order Status", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"status\": \"scheduled\",\n \"notes\": \"Appointment scheduled for tomorrow\"\n}" + }, + "url": { + "raw": "{{base_url}}{{api_prefix}}/lab-orders/1/status", + "host": ["{{base_url}}"], + "path": ["{{api_prefix}}", "lab-orders", "1", "status"] + } + }, + "response": [] + } + ] + }, + { + "name": "Lab Scheduling", + "item": [ + { + "name": "Create Schedule", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"order_id\": 1,\n \"technician_id\": 1,\n \"scheduled_datetime\": \"2024-01-15T10:00:00\",\n \"sample_type\": \"blood\",\n \"is_home_collection\": false,\n \"notes\": \"Patient prefers morning appointments\"\n}" + }, + "url": { + "raw": "{{base_url}}{{api_prefix}}/lab-schedule/", + "host": ["{{base_url}}"], + "path": ["{{api_prefix}}", "lab-schedule", ""] + } + }, + "response": [] + }, + { + "name": "Get Available Slots", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}{{api_prefix}}/lab-schedule/available-slots/2024-01-15", + "host": ["{{base_url}}"], + "path": ["{{api_prefix}}", "lab-schedule", "available-slots", "2024-01-15"] + } + }, + "response": [] + }, + { + "name": "Get Technician Schedule", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}{{api_prefix}}/lab-schedule/technician/1?schedule_date=2024-01-15", + "host": ["{{base_url}}"], + "path": ["{{api_prefix}}", "lab-schedule", "technician", "1"], + "query": [ + { + "key": "schedule_date", + "value": "2024-01-15" + } + ] + } + }, + "response": [] + }, + { + "name": "Update Schedule Status", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"status\": \"in_progress\",\n \"notes\": \"Sample collection started\"\n}" + }, + "url": { + "raw": "{{base_url}}{{api_prefix}}/lab-schedule/1/status", + "host": ["{{base_url}}"], + "path": ["{{api_prefix}}", "lab-schedule", "1", "status"] + } + }, + "response": [] + }, + { + "name": "Get Home Collections", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}{{api_prefix}}/lab-schedule/home-collections/?schedule_date=2024-01-15", + "host": ["{{base_url}}"], + "path": ["{{api_prefix}}", "lab-schedule", "home-collections", ""], + "query": [ + { + "key": "schedule_date", + "value": "2024-01-15" + } + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Lab Results", + "item": [ + { + "name": "Create Result", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"order_id\": 1,\n \"test_id\": 1,\n \"result_value\": \"7.2\",\n \"unit\": \"g/dL\",\n \"reference_range_min\": 6.0,\n \"reference_range_max\": 8.0,\n \"test_name\": \"Hemoglobin\",\n \"notes\": \"Normal result within reference range\"\n}" + }, + "url": { + "raw": "{{base_url}}{{api_prefix}}/lab-results/", + "host": ["{{base_url}}"], + "path": ["{{api_prefix}}", "lab-results", ""] + } + }, + "response": [] + }, + { + "name": "Get Result", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}{{api_prefix}}/lab-results/1", + "host": ["{{base_url}}"], + "path": ["{{api_prefix}}", "lab-results", "1"] + } + }, + "response": [] + }, + { + "name": "Get Results by Order", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}{{api_prefix}}/lab-results/order/1", + "host": ["{{base_url}}"], + "path": ["{{api_prefix}}", "lab-results", "order", "1"] + } + }, + "response": [] + }, + { + "name": "Verify Result", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"verified_by\": 1,\n \"verification_notes\": \"Result verified and approved\"\n}" + }, + "url": { + "raw": "{{base_url}}{{api_prefix}}/lab-results/1/verify", + "host": ["{{base_url}}"], + "path": ["{{api_prefix}}", "lab-results", "1", "verify"] + } + }, + "response": [] + }, + { + "name": "Get Abnormal Results", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}{{api_prefix}}/lab-results/abnormal/", + "host": ["{{base_url}}"], + "path": ["{{api_prefix}}", "lab-results", "abnormal", ""] + } + }, + "response": [] + }, + { + "name": "Get Pending Verification", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}{{api_prefix}}/lab-results/pending-verification/", + "host": ["{{base_url}}"], + "path": ["{{api_prefix}}", "lab-results", "pending-verification", ""] + } + }, + "response": [] + } + ] + }, + { + "name": "Lab Reports", + "item": [ + { + "name": "Create Report", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"order_id\": 1,\n \"summary\": \"Complete blood count results\",\n \"findings\": \"All values within normal limits\",\n \"recommendations\": \"Continue current health regimen\"\n}" + }, + "url": { + "raw": "{{base_url}}{{api_prefix}}/lab-reports/", + "host": ["{{base_url}}"], + "path": ["{{api_prefix}}", "lab-reports", ""] + } + }, + "response": [] + }, + { + "name": "Get Report", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}{{api_prefix}}/lab-reports/1", + "host": ["{{base_url}}"], + "path": ["{{api_prefix}}", "lab-reports", "1"] + } + }, + "response": [] + }, + { + "name": "Get Patient Reports", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}{{api_prefix}}/lab-reports/patient/1", + "host": ["{{base_url}}"], + "path": ["{{api_prefix}}", "lab-reports", "patient", "1"] + } + }, + "response": [] + }, + { + "name": "Finalize Report", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"finalized_by\": 1\n}" + }, + "url": { + "raw": "{{base_url}}{{api_prefix}}/lab-reports/1/finalize", + "host": ["{{base_url}}"], + "path": ["{{api_prefix}}", "lab-reports", "1", "finalize"] + } + }, + "response": [] + }, + { + "name": "Get Finalized Reports", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}{{api_prefix}}/lab-reports/finalized/", + "host": ["{{base_url}}"], + "path": ["{{api_prefix}}", "lab-reports", "finalized", ""] + } + }, + "response": [] + }, + { + "name": "Integrate with EMR", + "request": { + "method": "PUT", + "header": [], + "url": { + "raw": "{{base_url}}{{api_prefix}}/lab-reports/1/integrate-emr", + "host": ["{{base_url}}"], + "path": ["{{api_prefix}}", "lab-reports", "1", "integrate-emr"] + } + }, + "response": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/Lab-Scheduling/postman/Lab_Scheduling_Environment.postman_environment.json b/Lab-Scheduling/postman/Lab_Scheduling_Environment.postman_environment.json new file mode 100644 index 0000000..d8d8726 --- /dev/null +++ b/Lab-Scheduling/postman/Lab_Scheduling_Environment.postman_environment.json @@ -0,0 +1,73 @@ +{ + "id": "lab-scheduling-env", + "name": "Lab Scheduling Environment", + "values": [ + { + "key": "base_url", + "value": "http://localhost:8000", + "enabled": true, + "type": "default" + }, + { + "key": "api_prefix", + "value": "/api", + "enabled": true, + "type": "default" + }, + { + "key": "auth_token", + "value": "", + "enabled": true, + "type": "secret" + }, + { + "key": "patient_id", + "value": "1", + "enabled": true, + "type": "default" + }, + { + "key": "doctor_id", + "value": "1", + "enabled": true, + "type": "default" + }, + { + "key": "technician_id", + "value": "1", + "enabled": true, + "type": "default" + }, + { + "key": "order_id", + "value": "1", + "enabled": true, + "type": "default" + }, + { + "key": "schedule_id", + "value": "1", + "enabled": true, + "type": "default" + }, + { + "key": "result_id", + "value": "1", + "enabled": true, + "type": "default" + }, + { + "key": "report_id", + "value": "1", + "enabled": true, + "type": "default" + }, + { + "key": "test_date", + "value": "2024-01-15", + "enabled": true, + "type": "default" + } + ], + "_postman_variable_scope": "environment" +} \ No newline at end of file diff --git a/Lab-Scheduling/repositories/__init__.py b/Lab-Scheduling/repositories/__init__.py new file mode 100644 index 0000000..ad16e4e --- /dev/null +++ b/Lab-Scheduling/repositories/__init__.py @@ -0,0 +1,16 @@ +""" +Repository Layer +Data access layer for Lab Scheduling module +""" + +from .lab_order_repository import LabOrderRepository +from .lab_schedule_repository import LabScheduleRepository +from .lab_result_repository import LabResultRepository +from .lab_report_repository import LabReportRepository + +__all__ = [ + "LabOrderRepository", + "LabScheduleRepository", + "LabResultRepository", + "LabReportRepository" +] \ No newline at end of file diff --git a/Lab-Scheduling/repositories/lab_order_repository.py b/Lab-Scheduling/repositories/lab_order_repository.py new file mode 100644 index 0000000..63c58fe --- /dev/null +++ b/Lab-Scheduling/repositories/lab_order_repository.py @@ -0,0 +1,144 @@ +""" +Lab Order Repository +Data access layer for lab order operations +""" + +from typing import List, Optional +from sqlalchemy.orm import Session +from sqlalchemy import and_, or_, desc +from datetime import datetime + +from models.lab_order import LabOrder, OrderStatus, OrderPriority +from exceptions import LabOrderNotFoundException, DatabaseConnectionException + + +class LabOrderRepository: + """Repository for lab order data operations""" + + def __init__(self, db: Session): + self.db = db + + def create(self, lab_order_data: dict) -> LabOrder: + """Create a new lab order""" + try: + lab_order = LabOrder(**lab_order_data) + self.db.add(lab_order) + self.db.commit() + self.db.refresh(lab_order) + return lab_order + except Exception as e: + self.db.rollback() + raise DatabaseConnectionException(f"Failed to create lab order: {str(e)}") + + def get_by_id(self, order_id: int) -> LabOrder: + """Get lab order by ID""" + try: + lab_order = self.db.query(LabOrder).filter(LabOrder.id == order_id).first() + if not lab_order: + raise LabOrderNotFoundException(f"Lab order with ID {order_id} not found") + return lab_order + except LabOrderNotFoundException: + raise + except Exception as e: + raise DatabaseConnectionException(f"Failed to retrieve lab order: {str(e)}") + + def get_by_patient_id(self, patient_id: int) -> List[LabOrder]: + """Get all lab orders for a patient""" + try: + return self.db.query(LabOrder).filter(LabOrder.patient_id == patient_id).order_by(desc(LabOrder.created_at)).all() + except Exception as e: + raise DatabaseConnectionException(f"Failed to retrieve patient lab orders: {str(e)}") + + def get_by_doctor_id(self, doctor_id: int) -> List[LabOrder]: + """Get all lab orders by a doctor""" + try: + return self.db.query(LabOrder).filter(LabOrder.doctor_id == doctor_id).order_by(desc(LabOrder.created_at)).all() + except Exception as e: + raise DatabaseConnectionException(f"Failed to retrieve doctor lab orders: {str(e)}") + + def get_by_status(self, status: OrderStatus) -> List[LabOrder]: + """Get lab orders by status""" + try: + return self.db.query(LabOrder).filter(LabOrder.status == status).order_by(desc(LabOrder.created_at)).all() + except Exception as e: + raise DatabaseConnectionException(f"Failed to retrieve lab orders by status: {str(e)}") + + def get_by_priority(self, priority: OrderPriority) -> List[LabOrder]: + """Get lab orders by priority""" + try: + return self.db.query(LabOrder).filter(LabOrder.priority == priority).order_by(desc(LabOrder.created_at)).all() + except Exception as e: + raise DatabaseConnectionException(f"Failed to retrieve lab orders by priority: {str(e)}") + + def get_pending_orders(self) -> List[LabOrder]: + """Get all pending lab orders""" + try: + return self.db.query(LabOrder).filter( + LabOrder.status.in_([OrderStatus.PENDING, OrderStatus.SCHEDULED]) + ).order_by(LabOrder.priority.desc(), LabOrder.created_at).all() + except Exception as e: + raise DatabaseConnectionException(f"Failed to retrieve pending lab orders: {str(e)}") + + def update_status(self, order_id: int, status: OrderStatus) -> LabOrder: + """Update lab order status""" + try: + lab_order = self.get_by_id(order_id) + lab_order.status = status + lab_order.updated_at = datetime.utcnow() + self.db.commit() + self.db.refresh(lab_order) + return lab_order + except LabOrderNotFoundException: + raise + except Exception as e: + self.db.rollback() + raise DatabaseConnectionException(f"Failed to update lab order status: {str(e)}") + + def update(self, order_id: int, update_data: dict) -> LabOrder: + """Update lab order""" + try: + lab_order = self.get_by_id(order_id) + for key, value in update_data.items(): + if hasattr(lab_order, key): + setattr(lab_order, key, value) + lab_order.updated_at = datetime.utcnow() + self.db.commit() + self.db.refresh(lab_order) + return lab_order + except LabOrderNotFoundException: + raise + except Exception as e: + self.db.rollback() + raise DatabaseConnectionException(f"Failed to update lab order: {str(e)}") + + def delete(self, order_id: int) -> bool: + """Delete lab order""" + try: + lab_order = self.get_by_id(order_id) + self.db.delete(lab_order) + self.db.commit() + return True + except LabOrderNotFoundException: + raise + except Exception as e: + self.db.rollback() + raise DatabaseConnectionException(f"Failed to delete lab order: {str(e)}") + + def get_all(self, skip: int = 0, limit: int = 100) -> List[LabOrder]: + """Get all lab orders with pagination""" + try: + return self.db.query(LabOrder).order_by(desc(LabOrder.created_at)).offset(skip).limit(limit).all() + except Exception as e: + raise DatabaseConnectionException(f"Failed to retrieve lab orders: {str(e)}") + + def search(self, query: str) -> List[LabOrder]: + """Search lab orders by clinical notes or test names""" + try: + return self.db.query(LabOrder).filter( + or_( + LabOrder.clinical_notes.ilike(f"%{query}%"), + LabOrder.test_names.ilike(f"%{query}%") + ) + ).order_by(desc(LabOrder.created_at)).all() + except Exception as e: + raise DatabaseConnectionException(f"Failed to search lab orders: {str(e)}") \ No newline at end of file diff --git a/Lab-Scheduling/repositories/lab_report_repository.py b/Lab-Scheduling/repositories/lab_report_repository.py new file mode 100644 index 0000000..f6d16c9 --- /dev/null +++ b/Lab-Scheduling/repositories/lab_report_repository.py @@ -0,0 +1,181 @@ +""" +Lab Report Repository +Data access layer for lab report operations +""" + +from typing import List, Optional +from sqlalchemy.orm import Session +from sqlalchemy import and_, or_, desc +from datetime import datetime + +from models.lab_report import LabReport, ReportStatus +from exceptions import LabReportNotFoundException, DatabaseConnectionException + + +class LabReportRepository: + """Repository for lab report data operations""" + + def __init__(self, db: Session): + self.db = db + + def create(self, report_data: dict) -> LabReport: + """Create a new lab report""" + try: + report = LabReport(**report_data) + self.db.add(report) + self.db.commit() + self.db.refresh(report) + return report + except Exception as e: + self.db.rollback() + raise DatabaseConnectionException(f"Failed to create lab report: {str(e)}") + + def get_by_id(self, report_id: int) -> LabReport: + """Get lab report by ID""" + try: + report = self.db.query(LabReport).filter(LabReport.id == report_id).first() + if not report: + raise LabReportNotFoundException(f"Lab report with ID {report_id} not found") + return report + except LabReportNotFoundException: + raise + except Exception as e: + raise DatabaseConnectionException(f"Failed to retrieve lab report: {str(e)}") + + def get_by_order_id(self, order_id: int) -> Optional[LabReport]: + """Get lab report by order ID""" + try: + return self.db.query(LabReport).filter(LabReport.order_id == order_id).first() + except Exception as e: + raise DatabaseConnectionException(f"Failed to retrieve report by order: {str(e)}") + + def get_by_patient_id(self, patient_id: int) -> List[LabReport]: + """Get all lab reports for a patient""" + try: + from models.lab_order import LabOrder + return self.db.query(LabReport).join(LabOrder).filter( + LabOrder.patient_id == patient_id + ).order_by(desc(LabReport.created_at)).all() + except Exception as e: + raise DatabaseConnectionException(f"Failed to retrieve patient reports: {str(e)}") + + def get_by_status(self, status: ReportStatus) -> List[LabReport]: + """Get lab reports by status""" + try: + return self.db.query(LabReport).filter(LabReport.status == status).order_by(desc(LabReport.created_at)).all() + except Exception as e: + raise DatabaseConnectionException(f"Failed to retrieve reports by status: {str(e)}") + + def get_finalized_reports(self) -> List[LabReport]: + """Get all finalized lab reports""" + try: + return self.db.query(LabReport).filter(LabReport.status == ReportStatus.FINALIZED).order_by(desc(LabReport.finalized_at)).all() + except Exception as e: + raise DatabaseConnectionException(f"Failed to retrieve finalized reports: {str(e)}") + + def get_pending_finalization(self) -> List[LabReport]: + """Get reports pending finalization""" + try: + return self.db.query(LabReport).filter(LabReport.status == ReportStatus.PENDING_FINALIZATION).order_by(LabReport.created_at).all() + except Exception as e: + raise DatabaseConnectionException(f"Failed to retrieve pending finalization reports: {str(e)}") + + def finalize_report(self, report_id: int, finalized_by: int) -> LabReport: + """Finalize a lab report""" + try: + report = self.get_by_id(report_id) + report.status = ReportStatus.FINALIZED + report.finalized_by = finalized_by + report.finalized_at = datetime.utcnow() + report.updated_at = datetime.utcnow() + self.db.commit() + self.db.refresh(report) + return report + except LabReportNotFoundException: + raise + except Exception as e: + self.db.rollback() + raise DatabaseConnectionException(f"Failed to finalize report: {str(e)}") + + def update_status(self, report_id: int, status: ReportStatus) -> LabReport: + """Update report status""" + try: + report = self.get_by_id(report_id) + report.status = status + report.updated_at = datetime.utcnow() + self.db.commit() + self.db.refresh(report) + return report + except LabReportNotFoundException: + raise + except Exception as e: + self.db.rollback() + raise DatabaseConnectionException(f"Failed to update report status: {str(e)}") + + def update(self, report_id: int, update_data: dict) -> LabReport: + """Update lab report""" + try: + report = self.get_by_id(report_id) + for key, value in update_data.items(): + if hasattr(report, key): + setattr(report, key, value) + report.updated_at = datetime.utcnow() + self.db.commit() + self.db.refresh(report) + return report + except LabReportNotFoundException: + raise + except Exception as e: + self.db.rollback() + raise DatabaseConnectionException(f"Failed to update report: {str(e)}") + + def delete(self, report_id: int) -> bool: + """Delete lab report""" + try: + report = self.get_by_id(report_id) + self.db.delete(report) + self.db.commit() + return True + except LabReportNotFoundException: + raise + except Exception as e: + self.db.rollback() + raise DatabaseConnectionException(f"Failed to delete report: {str(e)}") + + def get_all(self, skip: int = 0, limit: int = 100) -> List[LabReport]: + """Get all lab reports with pagination""" + try: + return self.db.query(LabReport).order_by(desc(LabReport.created_at)).offset(skip).limit(limit).all() + except Exception as e: + raise DatabaseConnectionException(f"Failed to retrieve reports: {str(e)}") + + def get_reports_by_date_range(self, start_date: datetime, end_date: datetime) -> List[LabReport]: + """Get reports within date range""" + try: + return self.db.query(LabReport).filter( + and_( + LabReport.created_at >= start_date, + LabReport.created_at <= end_date + ) + ).order_by(desc(LabReport.created_at)).all() + except Exception as e: + raise DatabaseConnectionException(f"Failed to retrieve reports by date range: {str(e)}") + + def search_reports(self, query: str) -> List[LabReport]: + """Search reports by summary or findings""" + try: + return self.db.query(LabReport).filter( + or_( + LabReport.summary.ilike(f"%{query}%"), + LabReport.findings.ilike(f"%{query}%") + ) + ).order_by(desc(LabReport.created_at)).all() + except Exception as e: + raise DatabaseConnectionException(f"Failed to search reports: {str(e)}") + + def get_emr_integrated_reports(self) -> List[LabReport]: + """Get reports that have been integrated with EMR""" + try: + return self.db.query(LabReport).filter(LabReport.emr_integrated == True).order_by(desc(LabReport.emr_integration_date)).all() + except Exception as e: + raise DatabaseConnectionException(f"Failed to retrieve EMR integrated reports: {str(e)}") \ No newline at end of file diff --git a/Lab-Scheduling/repositories/lab_result_repository.py b/Lab-Scheduling/repositories/lab_result_repository.py new file mode 100644 index 0000000..03220e0 --- /dev/null +++ b/Lab-Scheduling/repositories/lab_result_repository.py @@ -0,0 +1,172 @@ +""" +Lab Result Repository +Data access layer for lab result operations +""" + +from typing import List, Optional +from sqlalchemy.orm import Session +from sqlalchemy import and_, or_, desc +from datetime import datetime + +from models.lab_result import LabResult, ResultStatus +from exceptions import LabResultNotFoundException, DatabaseConnectionException + + +class LabResultRepository: + """Repository for lab result data operations""" + + def __init__(self, db: Session): + self.db = db + + def create(self, result_data: dict) -> LabResult: + """Create a new lab result""" + try: + result = LabResult(**result_data) + self.db.add(result) + self.db.commit() + self.db.refresh(result) + return result + except Exception as e: + self.db.rollback() + raise DatabaseConnectionException(f"Failed to create lab result: {str(e)}") + + def get_by_id(self, result_id: int) -> LabResult: + """Get lab result by ID""" + try: + result = self.db.query(LabResult).filter(LabResult.id == result_id).first() + if not result: + raise LabResultNotFoundException(f"Lab result with ID {result_id} not found") + return result + except LabResultNotFoundException: + raise + except Exception as e: + raise DatabaseConnectionException(f"Failed to retrieve lab result: {str(e)}") + + def get_by_order_id(self, order_id: int) -> List[LabResult]: + """Get all lab results for an order""" + try: + return self.db.query(LabResult).filter(LabResult.order_id == order_id).order_by(LabResult.created_at).all() + except Exception as e: + raise DatabaseConnectionException(f"Failed to retrieve results by order: {str(e)}") + + def get_by_test_id(self, test_id: int) -> List[LabResult]: + """Get all results for a specific test""" + try: + return self.db.query(LabResult).filter(LabResult.test_id == test_id).order_by(desc(LabResult.created_at)).all() + except Exception as e: + raise DatabaseConnectionException(f"Failed to retrieve results by test: {str(e)}") + + def get_by_status(self, status: ResultStatus) -> List[LabResult]: + """Get lab results by status""" + try: + return self.db.query(LabResult).filter(LabResult.status == status).order_by(desc(LabResult.created_at)).all() + except Exception as e: + raise DatabaseConnectionException(f"Failed to retrieve results by status: {str(e)}") + + def get_abnormal_results(self) -> List[LabResult]: + """Get all abnormal lab results""" + try: + return self.db.query(LabResult).filter(LabResult.is_abnormal == True).order_by(desc(LabResult.created_at)).all() + except Exception as e: + raise DatabaseConnectionException(f"Failed to retrieve abnormal results: {str(e)}") + + def get_pending_verification(self) -> List[LabResult]: + """Get results pending verification""" + try: + return self.db.query(LabResult).filter(LabResult.status == ResultStatus.PENDING_VERIFICATION).order_by(LabResult.created_at).all() + except Exception as e: + raise DatabaseConnectionException(f"Failed to retrieve pending verification results: {str(e)}") + + def update_status(self, result_id: int, status: ResultStatus) -> LabResult: + """Update result status""" + try: + result = self.get_by_id(result_id) + result.status = status + result.updated_at = datetime.utcnow() + self.db.commit() + self.db.refresh(result) + return result + except LabResultNotFoundException: + raise + except Exception as e: + self.db.rollback() + raise DatabaseConnectionException(f"Failed to update result status: {str(e)}") + + def verify_result(self, result_id: int, verified_by: int, verification_notes: str = None) -> LabResult: + """Verify a lab result""" + try: + result = self.get_by_id(result_id) + result.status = ResultStatus.VERIFIED + result.verified_by = verified_by + result.verified_at = datetime.utcnow() + if verification_notes: + result.verification_notes = verification_notes + result.updated_at = datetime.utcnow() + self.db.commit() + self.db.refresh(result) + return result + except LabResultNotFoundException: + raise + except Exception as e: + self.db.rollback() + raise DatabaseConnectionException(f"Failed to verify result: {str(e)}") + + def update(self, result_id: int, update_data: dict) -> LabResult: + """Update lab result""" + try: + result = self.get_by_id(result_id) + for key, value in update_data.items(): + if hasattr(result, key): + setattr(result, key, value) + result.updated_at = datetime.utcnow() + self.db.commit() + self.db.refresh(result) + return result + except LabResultNotFoundException: + raise + except Exception as e: + self.db.rollback() + raise DatabaseConnectionException(f"Failed to update result: {str(e)}") + + def delete(self, result_id: int) -> bool: + """Delete lab result""" + try: + result = self.get_by_id(result_id) + self.db.delete(result) + self.db.commit() + return True + except LabResultNotFoundException: + raise + except Exception as e: + self.db.rollback() + raise DatabaseConnectionException(f"Failed to delete result: {str(e)}") + + def get_all(self, skip: int = 0, limit: int = 100) -> List[LabResult]: + """Get all lab results with pagination""" + try: + return self.db.query(LabResult).order_by(desc(LabResult.created_at)).offset(skip).limit(limit).all() + except Exception as e: + raise DatabaseConnectionException(f"Failed to retrieve results: {str(e)}") + + def search_by_value_range(self, test_id: int, min_value: float, max_value: float) -> List[LabResult]: + """Search results by value range""" + try: + return self.db.query(LabResult).filter( + and_( + LabResult.test_id == test_id, + LabResult.numeric_value >= min_value, + LabResult.numeric_value <= max_value + ) + ).order_by(desc(LabResult.created_at)).all() + except Exception as e: + raise DatabaseConnectionException(f"Failed to search results by value range: {str(e)}") + + def get_patient_results(self, patient_id: int) -> List[LabResult]: + """Get all results for a patient through their orders""" + try: + from models.lab_order import LabOrder + return self.db.query(LabResult).join(LabOrder).filter( + LabOrder.patient_id == patient_id + ).order_by(desc(LabResult.created_at)).all() + except Exception as e: + raise DatabaseConnectionException(f"Failed to retrieve patient results: {str(e)}") \ No newline at end of file diff --git a/Lab-Scheduling/repositories/lab_schedule_repository.py b/Lab-Scheduling/repositories/lab_schedule_repository.py new file mode 100644 index 0000000..ef9a972 --- /dev/null +++ b/Lab-Scheduling/repositories/lab_schedule_repository.py @@ -0,0 +1,207 @@ +""" +Lab Schedule Repository +Data access layer for lab schedule operations +""" + +from typing import List, Optional +from sqlalchemy.orm import Session +from sqlalchemy import and_, or_, desc, func +from datetime import datetime, date, time + +from models.lab_schedule import LabSchedule, ScheduleStatus, SampleType +from exceptions import LabScheduleNotFoundException, DatabaseConnectionException + + +class LabScheduleRepository: + """Repository for lab schedule data operations""" + + def __init__(self, db: Session): + self.db = db + + def create(self, schedule_data: dict) -> LabSchedule: + """Create a new lab schedule""" + try: + schedule = LabSchedule(**schedule_data) + self.db.add(schedule) + self.db.commit() + self.db.refresh(schedule) + return schedule + except Exception as e: + self.db.rollback() + raise DatabaseConnectionException(f"Failed to create lab schedule: {str(e)}") + + def get_by_id(self, schedule_id: int) -> LabSchedule: + """Get lab schedule by ID""" + try: + schedule = self.db.query(LabSchedule).filter(LabSchedule.id == schedule_id).first() + if not schedule: + raise LabScheduleNotFoundException(f"Lab schedule with ID {schedule_id} not found") + return schedule + except LabScheduleNotFoundException: + raise + except Exception as e: + raise DatabaseConnectionException(f"Failed to retrieve lab schedule: {str(e)}") + + def get_by_order_id(self, order_id: int) -> Optional[LabSchedule]: + """Get lab schedule by order ID""" + try: + return self.db.query(LabSchedule).filter(LabSchedule.order_id == order_id).first() + except Exception as e: + raise DatabaseConnectionException(f"Failed to retrieve lab schedule by order: {str(e)}") + + def get_by_technician_id(self, technician_id: int, schedule_date: date = None) -> List[LabSchedule]: + """Get lab schedules for a technician""" + try: + query = self.db.query(LabSchedule).filter(LabSchedule.technician_id == technician_id) + if schedule_date: + query = query.filter(func.date(LabSchedule.scheduled_datetime) == schedule_date) + return query.order_by(LabSchedule.scheduled_datetime).all() + except Exception as e: + raise DatabaseConnectionException(f"Failed to retrieve technician schedules: {str(e)}") + + def get_available_slots(self, schedule_date: date, technician_id: int = None) -> List[dict]: + """Get available time slots for scheduling""" + try: + # Get existing appointments for the date + query = self.db.query(LabSchedule).filter( + func.date(LabSchedule.scheduled_datetime) == schedule_date, + LabSchedule.status.in_([ScheduleStatus.SCHEDULED, ScheduleStatus.IN_PROGRESS]) + ) + + if technician_id: + query = query.filter(LabSchedule.technician_id == technician_id) + + existing_appointments = query.all() + + # Generate available slots (simplified logic) + # In a real implementation, this would consider technician working hours, + # break times, and other constraints + available_slots = [] + start_hour = 8 # 8 AM + end_hour = 17 # 5 PM + + for hour in range(start_hour, end_hour): + for minute in [0, 30]: # 30-minute slots + slot_time = time(hour, minute) + slot_datetime = datetime.combine(schedule_date, slot_time) + + # Check if slot is available + is_available = not any( + abs((apt.scheduled_datetime - slot_datetime).total_seconds()) < 1800 # 30 minutes + for apt in existing_appointments + ) + + if is_available: + available_slots.append({ + "datetime": slot_datetime, + "time": slot_time.strftime("%H:%M"), + "available": True + }) + + return available_slots + except Exception as e: + raise DatabaseConnectionException(f"Failed to get available slots: {str(e)}") + + def get_by_date_range(self, start_date: date, end_date: date) -> List[LabSchedule]: + """Get lab schedules within date range""" + try: + return self.db.query(LabSchedule).filter( + and_( + func.date(LabSchedule.scheduled_datetime) >= start_date, + func.date(LabSchedule.scheduled_datetime) <= end_date + ) + ).order_by(LabSchedule.scheduled_datetime).all() + except Exception as e: + raise DatabaseConnectionException(f"Failed to retrieve schedules by date range: {str(e)}") + + def get_by_status(self, status: ScheduleStatus) -> List[LabSchedule]: + """Get lab schedules by status""" + try: + return self.db.query(LabSchedule).filter(LabSchedule.status == status).order_by(LabSchedule.scheduled_datetime).all() + except Exception as e: + raise DatabaseConnectionException(f"Failed to retrieve schedules by status: {str(e)}") + + def get_home_collections(self, schedule_date: date = None) -> List[LabSchedule]: + """Get home collection schedules""" + try: + query = self.db.query(LabSchedule).filter(LabSchedule.is_home_collection == True) + if schedule_date: + query = query.filter(func.date(LabSchedule.scheduled_datetime) == schedule_date) + return query.order_by(LabSchedule.scheduled_datetime).all() + except Exception as e: + raise DatabaseConnectionException(f"Failed to retrieve home collections: {str(e)}") + + def update_status(self, schedule_id: int, status: ScheduleStatus) -> LabSchedule: + """Update schedule status""" + try: + schedule = self.get_by_id(schedule_id) + schedule.status = status + schedule.updated_at = datetime.utcnow() + self.db.commit() + self.db.refresh(schedule) + return schedule + except LabScheduleNotFoundException: + raise + except Exception as e: + self.db.rollback() + raise DatabaseConnectionException(f"Failed to update schedule status: {str(e)}") + + def update(self, schedule_id: int, update_data: dict) -> LabSchedule: + """Update lab schedule""" + try: + schedule = self.get_by_id(schedule_id) + for key, value in update_data.items(): + if hasattr(schedule, key): + setattr(schedule, key, value) + schedule.updated_at = datetime.utcnow() + self.db.commit() + self.db.refresh(schedule) + return schedule + except LabScheduleNotFoundException: + raise + except Exception as e: + self.db.rollback() + raise DatabaseConnectionException(f"Failed to update schedule: {str(e)}") + + def delete(self, schedule_id: int) -> bool: + """Delete lab schedule""" + try: + schedule = self.get_by_id(schedule_id) + self.db.delete(schedule) + self.db.commit() + return True + except LabScheduleNotFoundException: + raise + except Exception as e: + self.db.rollback() + raise DatabaseConnectionException(f"Failed to delete schedule: {str(e)}") + + def check_conflicts(self, technician_id: int, scheduled_datetime: datetime, duration_minutes: int = 30) -> bool: + """Check for scheduling conflicts""" + try: + start_time = scheduled_datetime + end_time = datetime.combine( + scheduled_datetime.date(), + (datetime.combine(date.today(), scheduled_datetime.time()) + + datetime.timedelta(minutes=duration_minutes)).time() + ) + + conflicts = self.db.query(LabSchedule).filter( + and_( + LabSchedule.technician_id == technician_id, + LabSchedule.scheduled_datetime >= start_time, + LabSchedule.scheduled_datetime < end_time, + LabSchedule.status.in_([ScheduleStatus.SCHEDULED, ScheduleStatus.IN_PROGRESS]) + ) + ).count() + + return conflicts > 0 + except Exception as e: + raise DatabaseConnectionException(f"Failed to check conflicts: {str(e)}") + + def get_all(self, skip: int = 0, limit: int = 100) -> List[LabSchedule]: + """Get all lab schedules with pagination""" + try: + return self.db.query(LabSchedule).order_by(LabSchedule.scheduled_datetime).offset(skip).limit(limit).all() + except Exception as e: + raise DatabaseConnectionException(f"Failed to retrieve schedules: {str(e)}") \ No newline at end of file diff --git a/Lab-Scheduling/requirements.txt b/Lab-Scheduling/requirements.txt new file mode 100644 index 0000000..82dab16 --- /dev/null +++ b/Lab-Scheduling/requirements.txt @@ -0,0 +1,14 @@ +fastapi==0.128.0 +uvicorn[standard]==0.40.0 +sqlalchemy==2.0.36 +psycopg2-binary==2.9.10 +pydantic==2.11.5 +pydantic-settings==2.9.1 +python-dotenv==1.1.0 +python-multipart==0.0.20 +email-validator==2.3.0 +passlib[bcrypt]==1.7.4 +python-jose[cryptography]==3.3.0 +alembic==1.14.0 +pytest==9.0.2 +httpx==0.28.1 \ No newline at end of file diff --git a/Lab-Scheduling/routes/__init__.py b/Lab-Scheduling/routes/__init__.py new file mode 100644 index 0000000..eb21369 --- /dev/null +++ b/Lab-Scheduling/routes/__init__.py @@ -0,0 +1,16 @@ +""" +Routes Layer +API endpoint definitions for Lab Scheduling module +""" + +from .lab_order_routes import router as lab_order_routes +from .lab_schedule_routes import router as lab_schedule_routes +from .lab_result_routes import router as lab_result_routes +from .lab_report_routes import router as lab_report_routes + +__all__ = [ + "lab_order_routes", + "lab_schedule_routes", + "lab_result_routes", + "lab_report_routes" +] \ No newline at end of file diff --git a/Lab-Scheduling/routes/lab_order_routes.py b/Lab-Scheduling/routes/lab_order_routes.py new file mode 100644 index 0000000..186c486 --- /dev/null +++ b/Lab-Scheduling/routes/lab_order_routes.py @@ -0,0 +1,357 @@ +""" +Lab Order Routes +FastAPI routes for lab order management +""" + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from typing import List, Optional +import logging + +from database import get_db +from controllers.lab_order_controller import LabOrderController +from schemas.lab_order import ( + LabOrderCreate, + LabOrderResponse, + LabOrderUpdate, + LabOrderStatusUpdate +) + +logger = logging.getLogger(__name__) + +# Create router +router = APIRouter( + prefix="/lab-orders", + tags=["Lab Orders"], + responses={404: {"description": "Not found"}} +) + +# Dependency to get controller +def get_lab_order_controller(db: Session = Depends(get_db)) -> LabOrderController: + return LabOrderController(db) + + +@router.post( + "/", + response_model=LabOrderResponse, + status_code=status.HTTP_201_CREATED, + summary="Create Lab Order", + description=""" + Create a new laboratory test order from a doctor for a patient. + + This endpoint allows doctors to order specific laboratory tests for their patients. + The order will be created with 'ordered' status and can later be scheduled for sample collection. + + **Required Fields:** + - patient_id: Valid patient identifier + - doctor_id: Valid doctor identifier + + **Optional Fields:** + - record_id: Link to specific medical record + - priority: Order priority (normal, urgent, stat) + - clinical_notes: Additional clinical information + + **Business Rules:** + - Patient and doctor must exist in the system + - Priority defaults to 'normal' if not specified + - Order date is automatically set to current timestamp + """, + responses={ + 201: { + "description": "Lab order created successfully", + "model": LabOrderResponse + }, + 400: { + "description": "Invalid input data or business rule violation", + "content": { + "application/json": { + "example": { + "error": { + "code": "VALIDATION_ERROR", + "message": "Invalid patient_id", + "details": "Patient with ID 123 does not exist" + } + } + } + } + }, + 422: { + "description": "Request validation failed", + "content": { + "application/json": { + "example": { + "error": { + "code": "VALIDATION_ERROR", + "message": "Request validation failed", + "details": [ + { + "loc": ["body", "patient_id"], + "msg": "ensure this value is greater than 0", + "type": "value_error.number.not_gt" + } + ] + } + } + } + } + } + } +) +async def create_lab_order( + lab_order_data: LabOrderCreate, + controller: LabOrderController = Depends(get_lab_order_controller) +): + """Create a new lab order""" + try: + logger.info(f"Creating lab order for patient {lab_order_data.patient_id}") + lab_order = controller.create_lab_order(lab_order_data) + return lab_order + except Exception as e: + logger.error(f"Failed to create lab order: {str(e)}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Failed to create lab order: {str(e)}" + ) + + +@router.get( + "/patient/{patient_id}", + response_model=List[LabOrderResponse], + summary="Get Patient Lab Orders", + description=""" + Retrieve all laboratory orders for a specific patient. + + This endpoint returns a list of all lab orders associated with a patient, + with optional filtering by order status. + + **Path Parameters:** + - patient_id: Patient identifier (must be positive integer) + + **Query Parameters:** + - status_filter: Optional status filter (ordered, scheduled, sample_collected, completed, cancelled) + + **Response:** + - Returns paginated list of lab orders + - Includes order details, priority, status, and timestamps + - Empty list if no orders found for patient + """, + responses={ + 200: { + "description": "List of patient lab orders", + "model": List[LabOrderResponse] + }, + 404: { + "description": "Patient not found or no orders exist", + "content": { + "application/json": { + "example": { + "error": { + "code": "PATIENT_NOT_FOUND", + "message": "Patient with ID 123 not found", + "details": "The specified patient does not exist in the system" + } + } + } + } + } + } +) +async def get_patient_lab_orders( + patient_id: int, + status_filter: Optional[str] = None, + controller: LabOrderController = Depends(get_lab_order_controller) +): + """Get all lab orders for a specific patient""" + try: + logger.info(f"Retrieving lab orders for patient {patient_id}") + lab_orders = controller.get_patient_lab_orders(patient_id, status_filter) + return lab_orders + except Exception as e: + logger.error(f"Failed to retrieve patient lab orders: {str(e)}") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Failed to retrieve lab orders: {str(e)}" + ) + + +@router.get( + "/{order_id}", + response_model=LabOrderResponse, + summary="Get Lab Order Details", + description=""" + Retrieve detailed information for a specific laboratory order. + + This endpoint returns complete details for a single lab order including + patient information, doctor details, test specifications, and current status. + + **Path Parameters:** + - order_id: Lab order identifier (must be positive integer) + + **Response:** + - Complete lab order information + - Includes all order details and metadata + - Returns 404 if order not found + """, + responses={ + 200: { + "description": "Lab order details", + "model": LabOrderResponse + }, + 404: { + "description": "Lab order not found", + "content": { + "application/json": { + "example": { + "error": { + "code": "LAB_ORDER_NOT_FOUND", + "message": "Lab order with ID 123 not found", + "details": "The specified lab order does not exist or has been deleted" + } + } + } + } + } + } +) +async def get_lab_order( + order_id: int, + controller: LabOrderController = Depends(get_lab_order_controller) +): + """Get a specific lab order by ID""" + try: + logger.info(f"Retrieving lab order {order_id}") + lab_order = controller.get_lab_order(order_id) + if not lab_order: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Lab order {order_id} not found" + ) + return lab_order + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to retrieve lab order: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to retrieve lab order: {str(e)}" + ) + + +@router.put( + "/{order_id}/status", + response_model=LabOrderResponse, + summary="Update Lab Order Status", + description=""" + Update the status of a laboratory order. + + This endpoint allows updating the status of a lab order as it progresses + through the workflow (ordered β†’ scheduled β†’ sample_collected β†’ completed). + + **Path Parameters:** + - order_id: Lab order identifier (must be positive integer) + + **Request Body:** + - status: New status value (ordered, scheduled, sample_collected, completed, cancelled) + - notes: Optional notes explaining the status change + + **Business Rules:** + - Status transitions must follow logical workflow + - Cannot change status of cancelled orders + - Completed orders cannot be modified + - Status change is logged with timestamp + """, + responses={ + 200: { + "description": "Lab order status updated successfully", + "model": LabOrderResponse + }, + 400: { + "description": "Invalid status transition or business rule violation", + "content": { + "application/json": { + "example": { + "error": { + "code": "INVALID_STATUS_TRANSITION", + "message": "Cannot change status from completed to ordered", + "details": "Status transitions must follow logical workflow" + } + } + } + } + }, + 404: { + "description": "Lab order not found", + "content": { + "application/json": { + "example": { + "error": { + "code": "LAB_ORDER_NOT_FOUND", + "message": "Lab order with ID 123 not found", + "details": "The specified lab order does not exist" + } + } + } + } + } + } +) +async def update_lab_order_status( + order_id: int, + status_update: LabOrderStatusUpdate, + controller: LabOrderController = Depends(get_lab_order_controller) +): + """Update lab order status""" + try: + logger.info(f"Updating status for lab order {order_id} to {status_update.status}") + lab_order = controller.update_lab_order_status(order_id, status_update.status) + if not lab_order: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Lab order {order_id} not found" + ) + return lab_order + except HTTPException: + raise + except Exception as e: + logger.error(f"Failed to update lab order status: {str(e)}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Failed to update lab order status: {str(e)}" + ) + + +@router.get("/doctor/{doctor_id}", response_model=List[LabOrderResponse]) +async def get_doctor_lab_orders( + doctor_id: int, + status_filter: Optional[str] = None, + controller: LabOrderController = Depends(get_lab_order_controller) +): + """Get all lab orders for a specific doctor""" + try: + logger.info(f"Retrieving lab orders for doctor {doctor_id}") + lab_orders = controller.get_doctor_lab_orders(doctor_id, status_filter) + return lab_orders + except Exception as e: + logger.error(f"Failed to retrieve doctor lab orders: {str(e)}") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Failed to retrieve lab orders: {str(e)}" + ) + + +@router.get("/priority/{priority}", response_model=List[LabOrderResponse]) +async def get_orders_by_priority( + priority: str, + controller: LabOrderController = Depends(get_lab_order_controller) +): + """Get lab orders by priority level""" + try: + logger.info(f"Retrieving lab orders with priority {priority}") + lab_orders = controller.get_orders_by_priority(priority) + return lab_orders + except Exception as e: + logger.error(f"Failed to retrieve orders by priority: {str(e)}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Failed to retrieve orders by priority: {str(e)}" + ) \ No newline at end of file diff --git a/Lab-Scheduling/routes/lab_report_routes.py b/Lab-Scheduling/routes/lab_report_routes.py new file mode 100644 index 0000000..1a3deaa --- /dev/null +++ b/Lab-Scheduling/routes/lab_report_routes.py @@ -0,0 +1,206 @@ +""" +Lab Report Routes +API endpoints for lab report operations +""" + +from typing import List +from fastapi import APIRouter, Depends, Query +from sqlalchemy.orm import Session +from datetime import datetime + +from database import get_db +from controllers.lab_report_controller import LabReportController +from schemas.lab_report import ( + LabReportCreate, + LabReportUpdate, + LabReportResponse, + LabReportFinalization +) +from models.lab_report import ReportStatus + +router = APIRouter(prefix="/lab-reports", tags=["Lab Reports"]) + +@router.post( + "/", + response_model=LabReportResponse, + status_code=201, + summary="Create Lab Report", + description="Generate a comprehensive lab report from verified test results", + response_description="Created lab report with summary and findings" +) +def create_report( + report_data: LabReportCreate, + db: Session = Depends(get_db) +): + """ + Generate a comprehensive lab report from verified test results. + + This endpoint creates professional lab reports with: + - Automated content generation from verified results + - Clinical summary and findings + - Abnormal value highlighting + - Recommendations based on results + - Integration-ready format for EMR systems + + **Requirements:** + - All test results must be verified + - Order must have completed sample collection + - Results must pass quality control checks + + **Report Contents:** + - Patient and order information + - Complete test result panel + - Reference ranges and abnormal flags + - Clinical interpretation and recommendations + """ + return LabReportController.create_report(report_data, db) + +@router.get( + "/{report_id}", + response_model=LabReportResponse, + summary="Get Lab Report", + description="Retrieve a specific lab report by ID", + response_description="Complete lab report with all sections" +) +def get_report( + report_id: int, + db: Session = Depends(get_db) +): + """ + Get detailed information about a specific lab report. + + Returns complete report information including: + - Patient demographics and order details + - Complete test result summary + - Clinical findings and interpretations + - Recommendations and follow-up suggestions + - Report status and finalization details + - EMR integration status + """ + return LabReportController.get_report(report_id, db) + +@router.get( + "/patient/{patient_id}", + response_model=List[LabReportResponse], + summary="Get Patient Reports", + description="Get all lab reports for a specific patient", + response_description="List of patient's lab reports ordered by date" +) +def get_patient_reports( + patient_id: int, + db: Session = Depends(get_db) +): + """ + Get all lab reports for a specific patient. + + This endpoint provides: + - Complete patient lab history + - Chronological report ordering + - Trend analysis capabilities + - Historical result comparison + + Useful for: + - Patient care continuity + - Clinical decision making + - Longitudinal health monitoring + - Medical record management + """ + return LabReportController.get_patient_reports(patient_id, db) + +@router.put( + "/{report_id}/finalize", + response_model=LabReportResponse, + summary="Finalize Lab Report", + description="Finalize a lab report for official release and EMR integration", + response_description="Finalized lab report ready for distribution" +) +def finalize_report( + report_id: int, + finalization: LabReportFinalization, + db: Session = Depends(get_db) +): + """ + Finalize a lab report for official release. + + **Finalization Process:** + - Final quality review and approval + - Digital signature and timestamp + - Report becomes immutable + - Triggers EMR integration workflow + - Enables patient and provider access + + **Requirements:** + - All results must remain verified + - Report content must be complete + - Only authorized personnel can finalize + - Finalization cannot be reversed + + **Post-Finalization:** + - Report is available for distribution + - EMR integration is enabled + - Patient portal access is granted + - Billing processes are triggered + """ + return LabReportController.finalize_report(report_id, finalization, db) + +@router.get( + "/finalized/", + response_model=List[LabReportResponse], + summary="Get Finalized Reports", + description="Get all finalized lab reports", + response_description="List of finalized reports ready for distribution" +) +def get_finalized_reports( + db: Session = Depends(get_db) +): + """ + Get all finalized lab reports. + + This endpoint provides: + - Official report archive + - Distribution-ready reports + - Quality assurance tracking + - Performance metrics data + + **Use Cases:** + - Report distribution management + - Quality control auditing + - Performance monitoring + - Compliance reporting + """ + return LabReportController.get_finalized_reports(db) + +@router.put( + "/{report_id}/integrate-emr", + response_model=LabReportResponse, + summary="Integrate with EMR", + description="Integrate finalized report with Electronic Medical Records system", + response_description="Report with EMR integration status updated" +) +def integrate_with_emr( + report_id: int, + db: Session = Depends(get_db) +): + """ + Integrate finalized report with EMR system. + + **Integration Process:** + - Validates report is finalized + - Formats data for EMR compatibility + - Transmits report to EMR system + - Updates integration status and timestamp + - Handles integration errors and retries + + **Requirements:** + - Report must be finalized + - EMR system must be available + - Patient must exist in EMR + - Integration credentials must be valid + + **Benefits:** + - Seamless clinical workflow + - Reduced manual data entry + - Improved care coordination + - Enhanced patient safety + """ + return LabReportController.integrate_with_emr(report_id, db) \ No newline at end of file diff --git a/Lab-Scheduling/routes/lab_result_routes.py b/Lab-Scheduling/routes/lab_result_routes.py new file mode 100644 index 0000000..560acc0 --- /dev/null +++ b/Lab-Scheduling/routes/lab_result_routes.py @@ -0,0 +1,182 @@ +""" +Lab Result Routes +API endpoints for lab result operations +""" + +from typing import List +from fastapi import APIRouter, Depends, Query +from sqlalchemy.orm import Session + +from database import get_db +from controllers.lab_result_controller import LabResultController +from schemas.lab_result import ( + LabResultCreate, + LabResultUpdate, + LabResultResponse, + LabResultVerification +) +from models.lab_result import ResultStatus + +router = APIRouter(prefix="/lab-results", tags=["Lab Results"]) + +@router.post( + "/", + response_model=LabResultResponse, + status_code=201, + summary="Create Lab Result", + description="Enter a new lab test result with validation and abnormal value detection", + response_description="Created lab result with validation status" +) +def create_result( + result_data: LabResultCreate, + db: Session = Depends(get_db) +): + """ + Enter a new lab test result. + + This endpoint allows lab staff to enter test results with: + - Automatic validation against reference ranges + - Abnormal value detection and flagging + - Support for both numeric and text results + - Quality control and verification workflow + + **Business Rules:** + - Order must be in IN_PROGRESS or COMPLETED status + - Result values are validated for format and content + - Abnormal results are automatically flagged + - All results require verification before reporting + """ + return LabResultController.create_result(result_data, db) + +@router.get( + "/{result_id}", + response_model=LabResultResponse, + summary="Get Lab Result", + description="Retrieve a specific lab result by ID", + response_description="Lab result details with verification status" +) +def get_result( + result_id: int, + db: Session = Depends(get_db) +): + """ + Get detailed information about a specific lab result. + + Returns complete result information including: + - Test result value and units + - Reference ranges and abnormal flags + - Verification status and notes + - Quality control information + - Timestamps for result entry and verification + """ + return LabResultController.get_result(result_id, db) + +@router.get( + "/order/{order_id}", + response_model=List[LabResultResponse], + summary="Get Results by Order", + description="Get all lab results for a specific order", + response_description="List of all results for the order" +) +def get_results_by_order( + order_id: int, + db: Session = Depends(get_db) +): + """ + Get all lab results for a specific order. + + This endpoint provides: + - Complete result panel for an order + - Results grouped by test type + - Verification status for each result + - Abnormal value highlighting + + Useful for: + - Result review and verification + - Report generation + - Quality assurance checks + """ + return LabResultController.get_results_by_order(order_id, db) + +@router.put( + "/{result_id}/verify", + response_model=LabResultResponse, + summary="Verify Lab Result", + description="Verify a lab result for accuracy and clinical validity", + response_description="Verified lab result" +) +def verify_result( + result_id: int, + verification: LabResultVerification, + db: Session = Depends(get_db) +): + """ + Verify a lab result for accuracy and clinical validity. + + **Verification Process:** + - Reviews result value against reference ranges + - Confirms abnormal value flags are appropriate + - Adds verification notes if needed + - Updates result status to VERIFIED + - Records verifying technician and timestamp + + **Requirements:** + - Result must be in PENDING_VERIFICATION status + - Only authorized personnel can verify results + - Verification notes are optional but recommended for abnormal results + """ + return LabResultController.verify_result(result_id, verification, db) + +@router.get( + "/abnormal/", + response_model=List[LabResultResponse], + summary="Get Abnormal Results", + description="Get all lab results flagged as abnormal", + response_description="List of abnormal lab results requiring attention" +) +def get_abnormal_results( + db: Session = Depends(get_db) +): + """ + Get all lab results flagged as abnormal. + + This endpoint helps with: + - Critical value management + - Quality assurance review + - Clinical decision support + - Priority result handling + + **Abnormal Result Criteria:** + - Values outside reference ranges + - Critical high or low values + - Unexpected result patterns + - Quality control failures + """ + return LabResultController.get_abnormal_results(db) + +@router.get( + "/pending-verification/", + response_model=List[LabResultResponse], + summary="Get Pending Verification", + description="Get all results awaiting verification", + response_description="List of results pending verification" +) +def get_pending_verification( + db: Session = Depends(get_db) +): + """ + Get all results awaiting verification. + + This endpoint supports: + - Verification workflow management + - Quality control processes + - Workload distribution + - Performance monitoring + + **Verification Queue:** + - Results ordered by entry time + - Priority given to abnormal results + - Batch verification capabilities + - Verification assignment tracking + """ + return LabResultController.get_pending_verification(db) \ No newline at end of file diff --git a/Lab-Scheduling/routes/lab_schedule_routes.py b/Lab-Scheduling/routes/lab_schedule_routes.py new file mode 100644 index 0000000..4e47caf --- /dev/null +++ b/Lab-Scheduling/routes/lab_schedule_routes.py @@ -0,0 +1,180 @@ +""" +Lab Schedule Routes +API endpoints for lab schedule operations +""" + +from typing import List +from fastapi import APIRouter, Depends, Query +from sqlalchemy.orm import Session +from datetime import date + +from database import get_db +from controllers.lab_schedule_controller import LabScheduleController +from schemas.lab_schedule import ( + LabScheduleCreate, + LabScheduleUpdate, + LabScheduleResponse, + LabScheduleStatusUpdate, + AvailableSlotResponse +) +from models.lab_schedule import ScheduleStatus + +router = APIRouter(prefix="/lab-schedule", tags=["Lab Scheduling"]) + +@router.post( + "/", + response_model=LabScheduleResponse, + status_code=201, + summary="Create Lab Schedule", + description="Create a new lab appointment schedule for a patient with an assigned technician", + response_description="Created lab schedule with appointment details" +) +def create_schedule( + schedule_data: LabScheduleCreate, + db: Session = Depends(get_db) +): + """ + Create a new lab schedule appointment. + + This endpoint allows scheduling lab appointments by: + - Linking to an existing lab order + - Assigning a technician + - Setting appointment date and time + - Specifying collection type (in-lab or home collection) + - Adding special instructions or notes + + **Business Rules:** + - Order must be in PENDING status + - Appointment time must be in the future + - Technician must be available at the requested time + - Working hours: 8 AM to 5 PM, Monday to Friday + """ + return LabScheduleController.create_schedule(schedule_data, db) + +@router.get( + "/{schedule_id}", + response_model=LabScheduleResponse, + summary="Get Lab Schedule", + description="Retrieve a specific lab schedule by ID", + response_description="Lab schedule details" +) +def get_schedule( + schedule_id: int, + db: Session = Depends(get_db) +): + """ + Get detailed information about a specific lab schedule. + + Returns complete schedule information including: + - Appointment date and time + - Assigned technician details + - Patient and order information + - Collection type and location + - Current status and notes + """ + return LabScheduleController.get_schedule(schedule_id, db) + +@router.get( + "/available-slots/{schedule_date}", + response_model=List[AvailableSlotResponse], + summary="Get Available Time Slots", + description="Get available appointment slots for a specific date", + response_description="List of available time slots" +) +def get_available_slots( + schedule_date: date, + technician_id: int = Query(None, description="Filter by specific technician"), + db: Session = Depends(get_db) +): + """ + Get available appointment time slots for scheduling. + + This endpoint helps with appointment booking by showing: + - Available 30-minute time slots + - Working hours: 8:00 AM to 5:00 PM + - Excludes weekends and holidays + - Filters out already booked appointments + + **Parameters:** + - `schedule_date`: Date to check availability + - `technician_id`: Optional filter for specific technician + """ + return LabScheduleController.get_available_slots(schedule_date, technician_id, db) + +@router.get( + "/technician/{technician_id}", + response_model=List[LabScheduleResponse], + summary="Get Technician Schedule", + description="Get all scheduled appointments for a specific technician", + response_description="List of technician's scheduled appointments" +) +def get_technician_schedule( + technician_id: int, + schedule_date: date = Query(None, description="Filter by specific date"), + db: Session = Depends(get_db) +): + """ + Get all scheduled appointments for a technician. + + Useful for: + - Technician daily schedule management + - Workload distribution analysis + - Appointment coordination + + **Parameters:** + - `technician_id`: ID of the technician + - `schedule_date`: Optional date filter (returns all dates if not specified) + """ + return LabScheduleController.get_technician_schedule(technician_id, schedule_date, db) + +@router.put( + "/{schedule_id}/status", + response_model=LabScheduleResponse, + summary="Update Schedule Status", + description="Update the status of a lab schedule appointment", + response_description="Updated lab schedule" +) +def update_schedule_status( + schedule_id: int, + status_update: LabScheduleStatusUpdate, + db: Session = Depends(get_db) +): + """ + Update the status of a lab schedule appointment. + + **Valid Status Transitions:** + - SCHEDULED β†’ IN_PROGRESS (when technician starts collection) + - SCHEDULED β†’ CANCELLED (if appointment is cancelled) + - IN_PROGRESS β†’ COMPLETED (when sample collection is finished) + - IN_PROGRESS β†’ CANCELLED (if collection cannot be completed) + + **Status Effects:** + - IN_PROGRESS: Updates related lab order status + - COMPLETED: Marks order as ready for result entry + - CANCELLED: Returns order to PENDING status for rescheduling + """ + return LabScheduleController.update_schedule_status(schedule_id, status_update, db) + +@router.get( + "/home-collections/", + response_model=List[LabScheduleResponse], + summary="Get Home Collection Schedules", + description="Get all home collection appointments", + response_description="List of home collection schedules" +) +def get_home_collections( + schedule_date: date = Query(None, description="Filter by specific date"), + db: Session = Depends(get_db) +): + """ + Get all home collection appointments. + + This endpoint is useful for: + - Home collection route planning + - Technician assignment for home visits + - Patient communication and coordination + + **Parameters:** + - `schedule_date`: Optional date filter for specific day's collections + """ + return LabScheduleController.get_home_collections(schedule_date, db) \ No newline at end of file diff --git a/Lab-Scheduling/sample_data.sql b/Lab-Scheduling/sample_data.sql new file mode 100644 index 0000000..43c384c --- /dev/null +++ b/Lab-Scheduling/sample_data.sql @@ -0,0 +1,54 @@ +-- Sample Data for Lab Scheduling Module +-- Insert test data for development and testing + +-- Insert Lab Technicians +INSERT INTO lab_technicians (technician_id, first_name, last_name, license_number, phone_number, email, specialization, is_active) VALUES +(1, 'Alice', 'Johnson', 'LT001', '555-0101', 'alice.johnson@hospital.com', 'Hematology', true), +(2, 'Bob', 'Smith', 'LT002', '555-0102', 'bob.smith@hospital.com', 'Chemistry', true), +(3, 'Carol', 'Davis', 'LT003', '555-0103', 'carol.davis@hospital.com', 'Microbiology', true), +(4, 'David', 'Wilson', 'LT004', '555-0104', 'david.wilson@hospital.com', 'Immunology', true); + +-- Insert Lab Tests +INSERT INTO lab_tests (test_id, test_name, test_code, category, description, sample_type, reference_range_min, reference_range_max, reference_range_text, turnaround_time_hours, is_active, requires_fasting, cost) VALUES +(1, 'Complete Blood Count', 'CBC', 'Hematology', 'Complete blood count with differential', 'Blood', NULL, NULL, 'See individual components', 4, true, false, 25.00), +(2, 'Blood Glucose', 'GLU', 'Chemistry', 'Fasting blood glucose level', 'Blood', 70, 100, '70-100 mg/dL', 2, true, true, 15.00), +(3, 'Lipid Panel', 'LIPID', 'Chemistry', 'Cholesterol and triglycerides', 'Blood', NULL, NULL, 'See individual components', 4, true, true, 45.00), +(4, 'Thyroid Stimulating Hormone', 'TSH', 'Endocrinology', 'TSH level', 'Blood', 0.4, 4.0, '0.4-4.0 mIU/L', 24, true, false, 35.00), +(5, 'Urinalysis', 'UA', 'Chemistry', 'Complete urinalysis', 'Urine', NULL, NULL, 'Normal', 2, true, false, 20.00); + +-- Insert Lab Orders +INSERT INTO lab_orders (lab_order_id, patient_id, doctor_id, record_id, order_date, priority, status, clinical_notes) VALUES +(1, 101, 201, 301, '2024-01-10 09:00:00', 'normal', 'completed', 'Routine annual checkup'), +(2, 102, 202, 302, '2024-01-11 10:30:00', 'urgent', 'sample_collected', 'Patient experiencing fatigue'), +(3, 103, 201, 303, '2024-01-12 14:15:00', 'normal', 'scheduled', 'Pre-operative testing'), +(4, 104, 203, 304, '2024-01-13 11:45:00', 'stat', 'ordered', 'Emergency department referral'), +(5, 105, 202, 305, '2024-01-14 08:30:00', 'normal', 'completed', 'Follow-up testing'); + +-- Insert Lab Schedules +INSERT INTO lab_schedules (schedule_id, lab_order_id, lab_id, technician_id, scheduled_date, start_time, end_time, sample_type, schedule_status, home_collection, notes) VALUES +(1, 1, 1, 1, '2024-01-15', '09:00:00', '09:30:00', 'Blood', 'completed', false, 'Patient arrived on time'), +(2, 2, 1, 2, '2024-01-16', '10:00:00', '10:30:00', 'Blood', 'completed', false, 'Urgent processing requested'), +(3, 3, 1, 1, '2024-01-17', '14:00:00', '14:30:00', 'Blood', 'scheduled', false, 'Pre-op patient'), +(4, 5, 1, 3, '2024-01-18', '08:30:00', '09:00:00', 'Blood', 'completed', true, 'Home collection completed'); + +-- Insert Lab Results +INSERT INTO lab_results (result_id, lab_order_id, test_id, result_date, result_value, reference_range, abnormal_flag, remarks, verified_at, verified_by) VALUES +(1, 1, 1, '2024-01-15', 'Normal', 'Normal ranges', 'normal', 'All parameters within normal limits', '2024-01-15 15:30:00', 301), +(2, 1, 2, '2024-01-15', '85', '70-100 mg/dL', 'normal', 'Fasting glucose normal', '2024-01-15 15:30:00', 301), +(3, 2, 2, '2024-01-16', '145', '70-100 mg/dL', 'high', 'Elevated glucose, recommend follow-up', '2024-01-16 16:00:00', 302), +(4, 2, 4, '2024-01-16', '8.5', '0.4-4.0 mIU/L', 'high', 'Elevated TSH, possible hypothyroidism', '2024-01-16 16:00:00', 302), +(5, 5, 3, '2024-01-18', 'Normal', 'See individual components', 'normal', 'Lipid panel within normal limits', '2024-01-18 14:00:00', 301); + +-- Insert Lab Reports +INSERT INTO lab_reports (lab_report_id, lab_order_id, report_date, status, summary, finalized_at, finalized_by) VALUES +(1, 1, '2024-01-15', 'finalized', 'All test results are within normal ranges. Patient shows good health indicators for annual checkup.', '2024-01-15 16:00:00', 301), +(2, 2, '2024-01-16', 'finalized', 'Elevated glucose and TSH levels detected. Recommend endocrinology consultation and dietary counseling.', '2024-01-16 17:00:00', 302), +(3, 5, '2024-01-18', 'finalized', 'Follow-up lipid panel shows improvement. Continue current treatment plan.', '2024-01-18 15:00:00', 301); + +-- Update sequences to avoid conflicts +SELECT setval('lab_technicians_technician_id_seq', 10); +SELECT setval('lab_tests_test_id_seq', 10); +SELECT setval('lab_orders_lab_order_id_seq', 10); +SELECT setval('lab_schedules_schedule_id_seq', 10); +SELECT setval('lab_results_result_id_seq', 10); +SELECT setval('lab_reports_lab_report_id_seq', 10); \ No newline at end of file diff --git a/Lab-Scheduling/schemas/__init__.py b/Lab-Scheduling/schemas/__init__.py new file mode 100644 index 0000000..40587b8 --- /dev/null +++ b/Lab-Scheduling/schemas/__init__.py @@ -0,0 +1 @@ +# Schemas package \ No newline at end of file diff --git a/Lab-Scheduling/schemas/base.py b/Lab-Scheduling/schemas/base.py new file mode 100644 index 0000000..84ca917 --- /dev/null +++ b/Lab-Scheduling/schemas/base.py @@ -0,0 +1,52 @@ +from pydantic import BaseModel +from typing import Optional, Any, Dict +from datetime import datetime + + +class BaseResponse(BaseModel): + """Base response model with common fields""" + success: bool = True + message: Optional[str] = None + timestamp: datetime = datetime.utcnow() + + +class ErrorResponse(BaseModel): + """Error response model""" + success: bool = False + error: Dict[str, Any] + timestamp: datetime = datetime.utcnow() + + class Config: + schema_extra = { + "example": { + "success": False, + "error": { + "code": "VALIDATION_ERROR", + "message": "Invalid input data", + "details": "Field 'patient_id' is required" + }, + "timestamp": "2024-01-01T12:00:00Z" + } + } + + +class PaginationParams(BaseModel): + """Pagination parameters""" + skip: int = 0 + limit: int = 100 + + class Config: + schema_extra = { + "example": { + "skip": 0, + "limit": 50 + } + } + + +class PaginatedResponse(BaseResponse): + """Paginated response model""" + total: int + skip: int + limit: int + has_more: bool \ No newline at end of file diff --git a/Lab-Scheduling/schemas/lab_order.py b/Lab-Scheduling/schemas/lab_order.py new file mode 100644 index 0000000..c3cbd64 --- /dev/null +++ b/Lab-Scheduling/schemas/lab_order.py @@ -0,0 +1,106 @@ +""" +Lab Order Schemas +Pydantic models for lab order request/response validation +""" + +from pydantic import BaseModel, Field +from typing import Optional +from datetime import datetime +from enum import Enum + +from .base import BaseResponse + + +class OrderPriorityEnum(str, Enum): + NORMAL = "normal" + URGENT = "urgent" + STAT = "stat" + + +class OrderStatusEnum(str, Enum): + PENDING = "pending" + SCHEDULED = "scheduled" + IN_PROGRESS = "in_progress" + COMPLETED = "completed" + CANCELLED = "cancelled" + + +class LabOrderCreate(BaseModel): + """Request model for creating lab orders""" + patient_id: int = Field(..., gt=0, description="Patient identifier") + doctor_id: int = Field(..., gt=0, description="Doctor identifier") + medical_record_id: Optional[int] = Field(None, gt=0, description="Medical record identifier") + test_names: str = Field(..., min_length=1, max_length=500, description="Test names") + priority: Optional[OrderPriorityEnum] = Field(OrderPriorityEnum.NORMAL, description="Order priority") + clinical_notes: Optional[str] = Field(None, max_length=1000, description="Clinical notes") + + class Config: + json_schema_extra = { + "example": { + "patient_id": 123, + "doctor_id": 456, + "medical_record_id": 789, + "test_names": "Complete Blood Count, Lipid Panel", + "priority": "normal", + "clinical_notes": "Routine blood work for annual checkup" + } + } + + +class LabOrderUpdate(BaseModel): + """Request model for updating lab orders""" + priority: Optional[OrderPriorityEnum] = None + clinical_notes: Optional[str] = Field(None, max_length=1000) + + class Config: + json_schema_extra = { + "example": { + "priority": "urgent", + "clinical_notes": "Patient experiencing symptoms, expedite processing" + } + } + + +class LabOrderStatusUpdate(BaseModel): + """Request model for updating lab order status""" + status: OrderStatusEnum = Field(..., description="New status") + notes: Optional[str] = Field(None, max_length=500, description="Status change notes") + + class Config: + json_schema_extra = { + "example": { + "status": "scheduled", + "notes": "Appointment scheduled for tomorrow morning" + } + } + + +class LabOrderResponse(BaseModel): + """Response model for lab order data""" + id: int + patient_id: int + doctor_id: int + medical_record_id: Optional[int] + test_names: str + priority: OrderPriorityEnum + status: OrderStatusEnum + clinical_notes: Optional[str] + created_at: datetime + updated_at: Optional[datetime] + + class Config: + from_attributes = True + json_schema_extra = { + "example": { + "id": 1, + "patient_id": 123, + "doctor_id": 456, + "medical_record_id": 789, + "test_names": "Complete Blood Count, Lipid Panel", + "priority": "normal", + "status": "pending", + "clinical_notes": "Routine blood work for annual checkup", + "created_at": "2024-01-01T10:00:00Z", + "updated_at": "2024-01-01T10:00:00Z" + } + } \ No newline at end of file diff --git a/Lab-Scheduling/schemas/lab_report.py b/Lab-Scheduling/schemas/lab_report.py new file mode 100644 index 0000000..8d22ab9 --- /dev/null +++ b/Lab-Scheduling/schemas/lab_report.py @@ -0,0 +1,97 @@ +""" +Lab Report Schemas +Pydantic models for lab report request/response validation +""" + +from pydantic import BaseModel, Field +from typing import Optional +from datetime import datetime +from enum import Enum + +from .base import BaseResponse + + +class ReportStatusEnum(str, Enum): + DRAFT = "draft" + PENDING_FINALIZATION = "pending_finalization" + FINALIZED = "finalized" + + +class LabReportCreate(BaseModel): + """Request model for creating lab reports""" + order_id: int = Field(..., gt=0, description="Lab order identifier") + summary: Optional[str] = Field(None, max_length=1000, description="Report summary") + findings: Optional[str] = Field(None, max_length=2000, description="Report findings") + recommendations: Optional[str] = Field(None, max_length=1000, description="Report recommendations") + + class Config: + json_schema_extra = { + "example": { + "order_id": 1, + "summary": "Complete blood count results", + "findings": "All values within normal limits", + "recommendations": "Continue current health regimen" + } + } + + +class LabReportUpdate(BaseModel): + """Request model for updating lab reports""" + summary: Optional[str] = Field(None, max_length=1000) + findings: Optional[str] = Field(None, max_length=2000) + recommendations: Optional[str] = Field(None, max_length=1000) + + class Config: + json_schema_extra = { + "example": { + "summary": "Updated complete blood count results", + "findings": "All values within normal limits with minor variations" + } + } + + +class LabReportFinalization(BaseModel): + """Request model for finalizing lab reports""" + finalized_by: int = Field(..., gt=0, description="Finalizer user ID") + + class Config: + json_schema_extra = { + "example": { + "finalized_by": 1 + } + } + + +class LabReportResponse(BaseModel): + """Response model for lab report data""" + id: int + order_id: int + summary: Optional[str] + findings: Optional[str] + recommendations: Optional[str] + status: ReportStatusEnum + finalized_at: Optional[datetime] + finalized_by: Optional[int] + emr_integrated: bool + emr_integration_date: Optional[datetime] + created_at: datetime + updated_at: Optional[datetime] + + class Config: + from_attributes = True + json_schema_extra = { + "example": { + "id": 1, + "order_id": 1, + "summary": "Complete blood count results", + "findings": "All values within normal limits", + "recommendations": "Continue current health regimen", + "status": "finalized", + "finalized_at": "2024-01-01T15:00:00Z", + "finalized_by": 1, + "emr_integrated": True, + "emr_integration_date": "2024-01-01T16:00:00Z", + "created_at": "2024-01-01T10:00:00Z", + "updated_at": "2024-01-01T15:00:00Z" + } + } \ No newline at end of file diff --git a/Lab-Scheduling/schemas/lab_result.py b/Lab-Scheduling/schemas/lab_result.py new file mode 100644 index 0000000..df0e391 --- /dev/null +++ b/Lab-Scheduling/schemas/lab_result.py @@ -0,0 +1,120 @@ +""" +Lab Result Schemas +Pydantic models for lab result request/response validation +""" + +from pydantic import BaseModel, Field +from typing import Optional +from datetime import datetime +from enum import Enum + +from .base import BaseResponse + + +class ResultStatusEnum(str, Enum): + PENDING_VERIFICATION = "pending_verification" + VERIFIED = "verified" + REJECTED = "rejected" + + +class LabResultCreate(BaseModel): + """Request model for creating lab results""" + order_id: int = Field(..., gt=0, description="Lab order identifier") + test_id: int = Field(..., gt=0, description="Test identifier") + result_value: str = Field(..., min_length=1, max_length=200, description="Result value") + unit: Optional[str] = Field(None, max_length=50, description="Unit of measurement") + reference_range_min: Optional[float] = Field(None, description="Reference range minimum") + reference_range_max: Optional[float] = Field(None, description="Reference range maximum") + test_name: Optional[str] = Field(None, max_length=200, description="Test name") + notes: Optional[str] = Field(None, max_length=500, description="Result notes") + + class Config: + json_schema_extra = { + "example": { + "order_id": 1, + "test_id": 1, + "result_value": "7.2", + "unit": "g/dL", + "reference_range_min": 6.0, + "reference_range_max": 8.0, + "test_name": "Hemoglobin", + "notes": "Normal result within reference range" + } + } + + +class LabResultUpdate(BaseModel): + """Request model for updating lab results""" + result_value: Optional[str] = Field(None, min_length=1, max_length=200) + unit: Optional[str] = Field(None, max_length=50) + reference_range_min: Optional[float] = None + reference_range_max: Optional[float] = None + test_name: Optional[str] = Field(None, max_length=200) + notes: Optional[str] = Field(None, max_length=500) + + class Config: + json_schema_extra = { + "example": { + "result_value": "7.5", + "notes": "Updated result after re-analysis" + } + } + + +class LabResultVerification(BaseModel): + """Request model for verifying lab results""" + verified_by: int = Field(..., gt=0, description="Verifier user ID") + verification_notes: Optional[str] = Field(None, max_length=500, description="Verification notes") + + class Config: + json_schema_extra = { + "example": { + "verified_by": 1, + "verification_notes": "Result verified and approved" + } + } + + +class LabResultResponse(BaseModel): + """Response model for lab result data""" + id: int + order_id: int + test_id: int + result_value: str + numeric_value: Optional[float] + unit: Optional[str] + reference_range_min: Optional[float] + reference_range_max: Optional[float] + status: ResultStatusEnum + is_abnormal: bool + test_name: Optional[str] + notes: Optional[str] + verified_at: Optional[datetime] + verified_by: Optional[int] + verification_notes: Optional[str] + created_at: datetime + updated_at: Optional[datetime] + + class Config: + from_attributes = True + json_schema_extra = { + "example": { + "id": 1, + "order_id": 1, + "test_id": 1, + "result_value": "7.2", + "numeric_value": 7.2, + "unit": "g/dL", + "reference_range_min": 6.0, + "reference_range_max": 8.0, + "status": "verified", + "is_abnormal": False, + "test_name": "Hemoglobin", + "notes": "Normal result within reference range", + "verified_at": "2024-01-01T12:00:00Z", + "verified_by": 1, + "verification_notes": "Result verified and approved", + "created_at": "2024-01-01T10:00:00Z", + "updated_at": "2024-01-01T12:00:00Z" + } + } \ No newline at end of file diff --git a/Lab-Scheduling/schemas/lab_schedule.py b/Lab-Scheduling/schemas/lab_schedule.py new file mode 100644 index 0000000..d2353d4 --- /dev/null +++ b/Lab-Scheduling/schemas/lab_schedule.py @@ -0,0 +1,126 @@ +""" +Lab Schedule Schemas +Pydantic models for lab schedule request/response validation +""" + +from pydantic import BaseModel, Field +from typing import Optional +from datetime import datetime, date +from enum import Enum + +from .base import BaseResponse + + +class ScheduleStatusEnum(str, Enum): + SCHEDULED = "scheduled" + IN_PROGRESS = "in_progress" + COMPLETED = "completed" + CANCELLED = "cancelled" + + +class SampleTypeEnum(str, Enum): + BLOOD = "blood" + URINE = "urine" + STOOL = "stool" + SALIVA = "saliva" + OTHER = "other" + + +class LabScheduleCreate(BaseModel): + """Request model for creating lab schedules""" + order_id: int = Field(..., gt=0, description="Lab order identifier") + technician_id: int = Field(..., gt=0, description="Technician identifier") + scheduled_datetime: datetime = Field(..., description="Scheduled appointment datetime") + sample_type: Optional[SampleTypeEnum] = Field(SampleTypeEnum.BLOOD, description="Sample type") + is_home_collection: Optional[bool] = Field(False, description="Home collection flag") + notes: Optional[str] = Field(None, max_length=500, description="Schedule notes") + + class Config: + json_schema_extra = { + "example": { + "order_id": 1, + "technician_id": 1, + "scheduled_datetime": "2024-01-15T10:00:00", + "sample_type": "blood", + "is_home_collection": False, + "notes": "Patient prefers morning appointments" + } + } + + +class LabScheduleUpdate(BaseModel): + """Request model for updating lab schedules""" + scheduled_datetime: Optional[datetime] = None + technician_id: Optional[int] = Field(None, gt=0) + sample_type: Optional[SampleTypeEnum] = None + is_home_collection: Optional[bool] = None + notes: Optional[str] = Field(None, max_length=500) + + class Config: + json_schema_extra = { + "example": { + "scheduled_datetime": "2024-01-16T14:00:00", + "notes": "Rescheduled due to patient request" + } + } + + +class LabScheduleStatusUpdate(BaseModel): + """Request model for updating lab schedule status""" + status: ScheduleStatusEnum = Field(..., description="New status") + notes: Optional[str] = Field(None, max_length=500, description="Status change notes") + + class Config: + json_schema_extra = { + "example": { + "status": "in_progress", + "notes": "Sample collection started" + } + } + + +class LabScheduleResponse(BaseModel): + """Response model for lab schedule data""" + id: int + order_id: int + technician_id: int + scheduled_datetime: datetime + sample_type: SampleTypeEnum + status: ScheduleStatusEnum + is_home_collection: bool + notes: Optional[str] + created_at: datetime + updated_at: Optional[datetime] + + class Config: + from_attributes = True + json_schema_extra = { + "example": { + "id": 1, + "order_id": 1, + "technician_id": 1, + "scheduled_datetime": "2024-01-15T10:00:00Z", + "sample_type": "blood", + "status": "scheduled", + "is_home_collection": False, + "notes": "Patient prefers morning appointments", + "created_at": "2024-01-01T10:00:00Z", + "updated_at": "2024-01-01T10:00:00Z" + } + } + + +class AvailableSlotResponse(BaseModel): + """Response model for available time slots""" + datetime: datetime + time: str + available: bool + + class Config: + json_schema_extra = { + "example": { + "datetime": "2024-01-15T10:00:00Z", + "time": "10:00", + "available": True + } + } \ No newline at end of file diff --git a/Lab-Scheduling/services/__init__.py b/Lab-Scheduling/services/__init__.py new file mode 100644 index 0000000..b5f6d0b --- /dev/null +++ b/Lab-Scheduling/services/__init__.py @@ -0,0 +1,16 @@ +""" +Service Layer +Business logic layer for Lab Scheduling module +""" + +from .lab_order_service import LabOrderService +from .lab_schedule_service import LabScheduleService +from .lab_result_service import LabResultService +from .lab_report_service import LabReportService + +__all__ = [ + "LabOrderService", + "LabScheduleService", + "LabResultService", + "LabReportService" +] \ No newline at end of file diff --git a/Lab-Scheduling/services/lab_order_service.py b/Lab-Scheduling/services/lab_order_service.py new file mode 100644 index 0000000..7c025fd --- /dev/null +++ b/Lab-Scheduling/services/lab_order_service.py @@ -0,0 +1,197 @@ +""" +Lab Order Service +Business logic layer for lab order operations +""" + +from typing import List, Optional +from sqlalchemy.orm import Session +from datetime import datetime + +from repositories.lab_order_repository import LabOrderRepository +from models.lab_order import LabOrder, OrderStatus, OrderPriority +from exceptions import ( + LabOrderNotFoundException, + LabSchedulingException, + DatabaseConnectionException +) + + +class LabOrderService: + """Service for lab order business logic""" + + def __init__(self, db: Session): + self.db = db + self.repository = LabOrderRepository(db) + + def create_order(self, order_data: dict) -> LabOrder: + """Create a new lab order with validation""" + try: + # Validate required fields + required_fields = ['patient_id', 'doctor_id', 'medical_record_id', 'test_names'] + for field in required_fields: + if field not in order_data or not order_data[field]: + raise LabSchedulingException(f"Missing required field: {field}") + + # Set default values + order_data.setdefault('status', OrderStatus.PENDING) + order_data.setdefault('priority', OrderPriority.NORMAL) + order_data.setdefault('created_at', datetime.utcnow()) + order_data.setdefault('updated_at', datetime.utcnow()) + + # Validate priority + if 'priority' in order_data: + if order_data['priority'] not in [p.value for p in OrderPriority]: + raise LabSchedulingException(f"Invalid priority: {order_data['priority']}") + + # Create the order + return self.repository.create(order_data) + + except LabSchedulingException: + raise + except Exception as e: + raise DatabaseConnectionException(f"Failed to create lab order: {str(e)}") + + def get_order(self, order_id: int) -> LabOrder: + """Get lab order by ID""" + return self.repository.get_by_id(order_id) + + def get_patient_orders(self, patient_id: int) -> List[LabOrder]: + """Get all orders for a patient""" + return self.repository.get_by_patient_id(patient_id) + + def get_doctor_orders(self, doctor_id: int) -> List[LabOrder]: + """Get all orders by a doctor""" + return self.repository.get_by_doctor_id(doctor_id) + + def get_orders_by_status(self, status: OrderStatus) -> List[LabOrder]: + """Get orders by status""" + return self.repository.get_by_status(status) + + def get_orders_by_priority(self, priority: OrderPriority) -> List[LabOrder]: + """Get orders by priority""" + return self.repository.get_by_priority(priority) + + def get_pending_orders(self) -> List[LabOrder]: + """Get all pending orders sorted by priority""" + return self.repository.get_pending_orders() + + def update_order_status(self, order_id: int, status: OrderStatus, notes: str = None) -> LabOrder: + """Update order status with business logic validation""" + try: + # Get current order + order = self.repository.get_by_id(order_id) + + # Validate status transition + if not self._is_valid_status_transition(order.status, status): + raise LabSchedulingException( + f"Invalid status transition from {order.status.value} to {status.value}" + ) + + # Update status + updated_order = self.repository.update_status(order_id, status) + + # Add notes if provided + if notes: + self.repository.update(order_id, {'clinical_notes': notes}) + + return updated_order + + except LabOrderNotFoundException: + raise + except LabSchedulingException: + raise + except Exception as e: + raise DatabaseConnectionException(f"Failed to update order status: {str(e)}") + + def update_order(self, order_id: int, update_data: dict) -> LabOrder: + """Update lab order with validation""" + try: + # Validate update data + if 'status' in update_data: + current_order = self.repository.get_by_id(order_id) + if not self._is_valid_status_transition(current_order.status, update_data['status']): + raise LabSchedulingException( + f"Invalid status transition from {current_order.status.value} to {update_data['status'].value}" + ) + + if 'priority' in update_data: + if update_data['priority'] not in [p.value for p in OrderPriority]: + raise LabSchedulingException(f"Invalid priority: {update_data['priority']}") + + # Update the order + return self.repository.update(order_id, update_data) + + except LabOrderNotFoundException: + raise + except LabSchedulingException: + raise + except Exception as e: + raise DatabaseConnectionException(f"Failed to update order: {str(e)}") + + def cancel_order(self, order_id: int, reason: str) -> LabOrder: + """Cancel a lab order""" + try: + order = self.repository.get_by_id(order_id) + + # Check if order can be cancelled + if order.status in [OrderStatus.COMPLETED, OrderStatus.CANCELLED]: + raise LabSchedulingException(f"Cannot cancel order with status {order.status.value}") + + # Update status and add cancellation reason + update_data = { + 'status': OrderStatus.CANCELLED, + 'clinical_notes': f"CANCELLED: {reason}. Previous notes: {order.clinical_notes or ''}" + } + + return self.repository.update(order_id, update_data) + + except LabOrderNotFoundException: + raise + except LabSchedulingException: + raise + except Exception as e: + raise DatabaseConnectionException(f"Failed to cancel order: {str(e)}") + + def delete_order(self, order_id: int) -> bool: + """Delete a lab order (only if not processed)""" + try: + order = self.repository.get_by_id(order_id) + + # Check if order can be deleted + if order.status not in [OrderStatus.PENDING, OrderStatus.CANCELLED]: + raise LabSchedulingException( + f"Cannot delete order with status {order.status.value}. Only pending or cancelled orders can be deleted." + ) + + return self.repository.delete(order_id) + + except LabOrderNotFoundException: + raise + except LabSchedulingException: + raise + except Exception as e: + raise DatabaseConnectionException(f"Failed to delete order: {str(e)}") + + def search_orders(self, query: str) -> List[LabOrder]: + """Search orders by clinical notes or test names""" + return self.repository.search(query) + + def get_all_orders(self, skip: int = 0, limit: int = 100) -> List[LabOrder]: + """Get all orders with pagination""" + return self.repository.get_all(skip, limit) + + def get_urgent_orders(self) -> List[LabOrder]: + """Get all urgent priority orders""" + return self.repository.get_by_priority(OrderPriority.URGENT) + + def _is_valid_status_transition(self, current_status: OrderStatus, new_status: OrderStatus) -> bool: + """Validate if status transition is allowed""" + valid_transitions = { + OrderStatus.PENDING: [OrderStatus.SCHEDULED, OrderStatus.CANCELLED], + OrderStatus.SCHEDULED: [OrderStatus.IN_PROGRESS, OrderStatus.CANCELLED], + OrderStatus.IN_PROGRESS: [OrderStatus.COMPLETED, OrderStatus.CANCELLED], + OrderStatus.COMPLETED: [], # Final state + OrderStatus.CANCELLED: [] # Final state + } + + return new_status in valid_transitions.get(current_status, []) \ No newline at end of file diff --git a/Lab-Scheduling/services/lab_report_service.py b/Lab-Scheduling/services/lab_report_service.py new file mode 100644 index 0000000..6809c69 --- /dev/null +++ b/Lab-Scheduling/services/lab_report_service.py @@ -0,0 +1,309 @@ +""" +Lab Report Service +Business logic layer for lab report operations +""" + +from typing import List, Optional +from sqlalchemy.orm import Session +from datetime import datetime + +from repositories.lab_report_repository import LabReportRepository +from repositories.lab_result_repository import LabResultRepository +from repositories.lab_order_repository import LabOrderRepository +from models.lab_report import LabReport, ReportStatus +from models.lab_result import ResultStatus +from models.lab_order import OrderStatus +from exceptions import ( + LabReportNotFoundException, + LabOrderNotFoundException, + ReportFinalizedException, + LabSchedulingException, + DatabaseConnectionException +) + + +class LabReportService: + """Service for lab report business logic""" + + def __init__(self, db: Session): + self.db = db + self.repository = LabReportRepository(db) + self.result_repository = LabResultRepository(db) + self.order_repository = LabOrderRepository(db) + + def create_report(self, report_data: dict) -> LabReport: + """Create a new lab report with validation""" + try: + # Validate required fields + required_fields = ['order_id'] + for field in required_fields: + if field not in report_data or not report_data[field]: + raise LabSchedulingException(f"Missing required field: {field}") + + # Validate order exists and has results + order = self.order_repository.get_by_id(report_data['order_id']) + results = self.result_repository.get_by_order_id(report_data['order_id']) + + if not results: + raise LabSchedulingException("Cannot create report for order without results") + + # Check if all results are verified + unverified_results = [r for r in results if r.status != ResultStatus.VERIFIED] + if unverified_results: + raise LabSchedulingException("Cannot create report with unverified results") + + # Generate report content + report_content = self._generate_report_content(order, results) + + # Set default values + report_data.setdefault('status', ReportStatus.DRAFT) + report_data.setdefault('summary', report_content['summary']) + report_data.setdefault('findings', report_content['findings']) + report_data.setdefault('recommendations', report_content['recommendations']) + report_data.setdefault('created_at', datetime.utcnow()) + report_data.setdefault('updated_at', datetime.utcnow()) + + # Create the report + report = self.repository.create(report_data) + + return report + + except (LabOrderNotFoundException, LabSchedulingException): + raise + except Exception as e: + raise DatabaseConnectionException(f"Failed to create lab report: {str(e)}") + + def get_report(self, report_id: int) -> LabReport: + """Get lab report by ID""" + return self.repository.get_by_id(report_id) + + def get_report_by_order(self, order_id: int) -> Optional[LabReport]: + """Get report for a specific order""" + return self.repository.get_by_order_id(order_id) + + def get_patient_reports(self, patient_id: int) -> List[LabReport]: + """Get all reports for a patient""" + return self.repository.get_by_patient_id(patient_id) + + def get_reports_by_status(self, status: ReportStatus) -> List[LabReport]: + """Get reports by status""" + return self.repository.get_by_status(status) + + def get_finalized_reports(self) -> List[LabReport]: + """Get all finalized reports""" + return self.repository.get_finalized_reports() + + def get_pending_finalization(self) -> List[LabReport]: + """Get reports pending finalization""" + return self.repository.get_pending_finalization() + + def finalize_report(self, report_id: int, finalized_by: int) -> LabReport: + """Finalize a lab report""" + try: + # Get current report + report = self.repository.get_by_id(report_id) + + # Check if report can be finalized + if report.status == ReportStatus.FINALIZED: + raise ReportFinalizedException("Report is already finalized") + + # Validate that all results are still verified + results = self.result_repository.get_by_order_id(report.order_id) + unverified_results = [r for r in results if r.status != ResultStatus.VERIFIED] + if unverified_results: + raise LabSchedulingException("Cannot finalize report with unverified results") + + # Finalize the report + finalized_report = self.repository.finalize_report(report_id, finalized_by) + + # Update order status to completed if not already + order = self.order_repository.get_by_id(report.order_id) + if order.status != OrderStatus.COMPLETED: + self.order_repository.update_status(report.order_id, OrderStatus.COMPLETED) + + return finalized_report + + except (LabReportNotFoundException, ReportFinalizedException): + raise + except LabSchedulingException: + raise + except Exception as e: + raise DatabaseConnectionException(f"Failed to finalize report: {str(e)}") + + def update_report_status(self, report_id: int, status: ReportStatus) -> LabReport: + """Update report status with validation""" + try: + # Get current report + report = self.repository.get_by_id(report_id) + + # Check if report is finalized + if report.status == ReportStatus.FINALIZED: + raise ReportFinalizedException("Cannot update finalized report") + + # Validate status transition + if not self._is_valid_status_transition(report.status, status): + raise LabSchedulingException( + f"Invalid status transition from {report.status.value} to {status.value}" + ) + + return self.repository.update_status(report_id, status) + + except (LabReportNotFoundException, ReportFinalizedException): + raise + except LabSchedulingException: + raise + except Exception as e: + raise DatabaseConnectionException(f"Failed to update report status: {str(e)}") + + def update_report(self, report_id: int, update_data: dict) -> LabReport: + """Update lab report with validation""" + try: + # Get current report + current_report = self.repository.get_by_id(report_id) + + # Check if report is finalized + if current_report.status == ReportStatus.FINALIZED: + raise ReportFinalizedException("Cannot update finalized report") + + # Validate status transition if status is being updated + if 'status' in update_data: + if not self._is_valid_status_transition(current_report.status, update_data['status']): + raise LabSchedulingException( + f"Invalid status transition from {current_report.status.value} to {update_data['status'].value}" + ) + + return self.repository.update(report_id, update_data) + + except (LabReportNotFoundException, ReportFinalizedException): + raise + except LabSchedulingException: + raise + except Exception as e: + raise DatabaseConnectionException(f"Failed to update report: {str(e)}") + + def delete_report(self, report_id: int) -> bool: + """Delete a lab report (only if not finalized)""" + try: + report = self.repository.get_by_id(report_id) + + # Check if report can be deleted + if report.status == ReportStatus.FINALIZED: + raise ReportFinalizedException("Cannot delete finalized report") + + return self.repository.delete(report_id) + + except (LabReportNotFoundException, ReportFinalizedException): + raise + except Exception as e: + raise DatabaseConnectionException(f"Failed to delete report: {str(e)}") + + def get_all_reports(self, skip: int = 0, limit: int = 100) -> List[LabReport]: + """Get all reports with pagination""" + return self.repository.get_all(skip, limit) + + def get_reports_by_date_range(self, start_date: datetime, end_date: datetime) -> List[LabReport]: + """Get reports within date range""" + return self.repository.get_reports_by_date_range(start_date, end_date) + + def search_reports(self, query: str) -> List[LabReport]: + """Search reports by summary or findings""" + return self.repository.search_reports(query) + + def integrate_with_emr(self, report_id: int) -> LabReport: + """Integrate report with EMR system""" + try: + report = self.repository.get_by_id(report_id) + + # Check if report is finalized + if report.status != ReportStatus.FINALIZED: + raise LabSchedulingException("Only finalized reports can be integrated with EMR") + + # Simulate EMR integration (in real implementation, this would call EMR API) + update_data = { + 'emr_integrated': True, + 'emr_integration_date': datetime.utcnow() + } + + return self.repository.update(report_id, update_data) + + except LabReportNotFoundException: + raise + except LabSchedulingException: + raise + except Exception as e: + raise DatabaseConnectionException(f"Failed to integrate with EMR: {str(e)}") + + def get_emr_integrated_reports(self) -> List[LabReport]: + """Get reports that have been integrated with EMR""" + return self.repository.get_emr_integrated_reports() + + def regenerate_report(self, report_id: int) -> LabReport: + """Regenerate report content based on current results""" + try: + report = self.repository.get_by_id(report_id) + + # Check if report can be regenerated + if report.status == ReportStatus.FINALIZED: + raise ReportFinalizedException("Cannot regenerate finalized report") + + # Get current results + results = self.result_repository.get_by_order_id(report.order_id) + order = self.order_repository.get_by_id(report.order_id) + + # Generate new content + report_content = self._generate_report_content(order, results) + + # Update report + update_data = { + 'summary': report_content['summary'], + 'findings': report_content['findings'], + 'recommendations': report_content['recommendations'] + } + + return self.repository.update(report_id, update_data) + + except (LabReportNotFoundException, ReportFinalizedException): + raise + except Exception as e: + raise DatabaseConnectionException(f"Failed to regenerate report: {str(e)}") + + def _generate_report_content(self, order, results) -> dict: + """Generate report content based on order and results""" + # Count abnormal results + abnormal_results = [r for r in results if r.is_abnormal] + + # Generate summary + summary = f"Laboratory report for {len(results)} test(s). " + if abnormal_results: + summary += f"{len(abnormal_results)} abnormal result(s) found." + else: + summary += "All results within normal ranges." + + # Generate findings + findings = "Test Results:\n" + for result in results: + status_indicator = "⚠️ ABNORMAL" if result.is_abnormal else "βœ“ Normal" + findings += f"- {result.test_name or f'Test ID {result.test_id}'}: {result.result_value} {result.unit or ''} [{status_indicator}]\n" + + # Generate recommendations + recommendations = "" + if abnormal_results: + recommendations = "Recommendations:\n- Follow up with ordering physician for abnormal results\n- Consider repeat testing if clinically indicated" + else: + recommendations = "No specific recommendations. Results are within normal limits." + + return { + 'summary': summary, + 'findings': findings, + 'recommendations': recommendations + } + + def _is_valid_status_transition(self, current_status: ReportStatus, new_status: ReportStatus) -> bool: + """Validate if status transition is allowed""" + valid_transitions = { + ReportStatus.DRAFT: [ReportStatus.PENDING_FINALIZATION], + ReportStatus.PENDING_FINALIZATION: [ReportStatus.FINALIZED, ReportStatus.DRAFT], + ReportStatus.FINALIZED: [] # Final state + } + + return new_status in valid_transitions.get(current_status, []) \ No newline at end of file diff --git a/Lab-Scheduling/services/lab_result_service.py b/Lab-Scheduling/services/lab_result_service.py new file mode 100644 index 0000000..de7929e --- /dev/null +++ b/Lab-Scheduling/services/lab_result_service.py @@ -0,0 +1,259 @@ +""" +Lab Result Service +Business logic layer for lab result operations +""" + +from typing import List, Optional +from sqlalchemy.orm import Session +from datetime import datetime + +from repositories.lab_result_repository import LabResultRepository +from repositories.lab_order_repository import LabOrderRepository +from models.lab_result import LabResult, ResultStatus +from models.lab_order import OrderStatus +from exceptions import ( + LabResultNotFoundException, + LabOrderNotFoundException, + ResultValidationException, + LabSchedulingException, + DatabaseConnectionException +) + + +class LabResultService: + """Service for lab result business logic""" + + def __init__(self, db: Session): + self.db = db + self.repository = LabResultRepository(db) + self.order_repository = LabOrderRepository(db) + + def create_result(self, result_data: dict) -> LabResult: + """Create a new lab result with validation""" + try: + # Validate required fields + required_fields = ['order_id', 'test_id', 'result_value'] + for field in required_fields: + if field not in result_data or result_data[field] is None: + raise LabSchedulingException(f"Missing required field: {field}") + + # Validate order exists and is in correct status + order = self.order_repository.get_by_id(result_data['order_id']) + if order.status not in [OrderStatus.IN_PROGRESS, OrderStatus.COMPLETED]: + raise LabSchedulingException(f"Cannot add results for order with status {order.status.value}") + + # Validate and process result value + self._validate_result_value(result_data) + + # Set default values + result_data.setdefault('status', ResultStatus.PENDING_VERIFICATION) + result_data.setdefault('created_at', datetime.utcnow()) + result_data.setdefault('updated_at', datetime.utcnow()) + + # Determine if result is abnormal + result_data['is_abnormal'] = self._check_abnormal_result(result_data) + + # Create the result + result = self.repository.create(result_data) + + return result + + except (LabOrderNotFoundException, LabSchedulingException): + raise + except Exception as e: + raise DatabaseConnectionException(f"Failed to create lab result: {str(e)}") + + def get_result(self, result_id: int) -> LabResult: + """Get lab result by ID""" + return self.repository.get_by_id(result_id) + + def get_results_by_order(self, order_id: int) -> List[LabResult]: + """Get all results for an order""" + return self.repository.get_by_order_id(order_id) + + def get_results_by_test(self, test_id: int) -> List[LabResult]: + """Get all results for a specific test""" + return self.repository.get_by_test_id(test_id) + + def get_results_by_status(self, status: ResultStatus) -> List[LabResult]: + """Get results by status""" + return self.repository.get_by_status(status) + + def get_abnormal_results(self) -> List[LabResult]: + """Get all abnormal results""" + return self.repository.get_abnormal_results() + + def get_pending_verification(self) -> List[LabResult]: + """Get results pending verification""" + return self.repository.get_pending_verification() + + def verify_result(self, result_id: int, verified_by: int, verification_notes: str = None) -> LabResult: + """Verify a lab result""" + try: + # Get current result + result = self.repository.get_by_id(result_id) + + # Check if result can be verified + if result.status != ResultStatus.PENDING_VERIFICATION: + raise LabSchedulingException(f"Cannot verify result with status {result.status.value}") + + # Verify the result + verified_result = self.repository.verify_result(result_id, verified_by, verification_notes) + + return verified_result + + except LabResultNotFoundException: + raise + except LabSchedulingException: + raise + except Exception as e: + raise DatabaseConnectionException(f"Failed to verify result: {str(e)}") + + def update_result_status(self, result_id: int, status: ResultStatus) -> LabResult: + """Update result status with validation""" + try: + # Get current result + result = self.repository.get_by_id(result_id) + + # Validate status transition + if not self._is_valid_status_transition(result.status, status): + raise LabSchedulingException( + f"Invalid status transition from {result.status.value} to {status.value}" + ) + + return self.repository.update_status(result_id, status) + + except LabResultNotFoundException: + raise + except LabSchedulingException: + raise + except Exception as e: + raise DatabaseConnectionException(f"Failed to update result status: {str(e)}") + + def update_result(self, result_id: int, update_data: dict) -> LabResult: + """Update lab result with validation""" + try: + # Get current result + current_result = self.repository.get_by_id(result_id) + + # Check if result can be updated + if current_result.status == ResultStatus.VERIFIED: + raise LabSchedulingException("Cannot update verified results") + + # Validate result value if being updated + if 'result_value' in update_data: + self._validate_result_value(update_data) + # Re-check if result is abnormal + update_data['is_abnormal'] = self._check_abnormal_result(update_data) + + # Validate status transition if status is being updated + if 'status' in update_data: + if not self._is_valid_status_transition(current_result.status, update_data['status']): + raise LabSchedulingException( + f"Invalid status transition from {current_result.status.value} to {update_data['status'].value}" + ) + + return self.repository.update(result_id, update_data) + + except LabResultNotFoundException: + raise + except LabSchedulingException: + raise + except Exception as e: + raise DatabaseConnectionException(f"Failed to update result: {str(e)}") + + def delete_result(self, result_id: int) -> bool: + """Delete a lab result (only if not verified)""" + try: + result = self.repository.get_by_id(result_id) + + # Check if result can be deleted + if result.status == ResultStatus.VERIFIED: + raise LabSchedulingException("Cannot delete verified results") + + return self.repository.delete(result_id) + + except LabResultNotFoundException: + raise + except LabSchedulingException: + raise + except Exception as e: + raise DatabaseConnectionException(f"Failed to delete result: {str(e)}") + + def get_patient_results(self, patient_id: int) -> List[LabResult]: + """Get all results for a patient""" + return self.repository.get_patient_results(patient_id) + + def search_results_by_value_range(self, test_id: int, min_value: float, max_value: float) -> List[LabResult]: + """Search results by value range""" + return self.repository.search_by_value_range(test_id, min_value, max_value) + + def get_all_results(self, skip: int = 0, limit: int = 100) -> List[LabResult]: + """Get all results with pagination""" + return self.repository.get_all(skip, limit) + + def bulk_verify_results(self, result_ids: List[int], verified_by: int) -> List[LabResult]: + """Verify multiple results at once""" + verified_results = [] + for result_id in result_ids: + try: + verified_result = self.verify_result(result_id, verified_by) + verified_results.append(verified_result) + except LabSchedulingException: + # Skip results that cannot be verified + continue + return verified_results + + def _validate_result_value(self, result_data: dict) -> None: + """Validate result value format and content""" + result_value = result_data['result_value'] + + if not result_value or str(result_value).strip() == '': + raise ResultValidationException("Result value cannot be empty") + + # Try to convert to numeric if possible + try: + numeric_value = float(result_value) + result_data['numeric_value'] = numeric_value + except (ValueError, TypeError): + # Non-numeric result, store as text + result_data['numeric_value'] = None + + # Validate reference ranges if provided + if 'reference_range_min' in result_data and 'reference_range_max' in result_data: + try: + min_val = float(result_data['reference_range_min']) + max_val = float(result_data['reference_range_max']) + if min_val >= max_val: + raise ResultValidationException("Invalid reference range: minimum must be less than maximum") + except (ValueError, TypeError): + raise ResultValidationException("Reference range values must be numeric") + + def _check_abnormal_result(self, result_data: dict) -> bool: + """Check if result is abnormal based on reference ranges""" + if 'numeric_value' not in result_data or result_data['numeric_value'] is None: + return False + + numeric_value = result_data['numeric_value'] + min_range = result_data.get('reference_range_min') + max_range = result_data.get('reference_range_max') + + if min_range is not None and max_range is not None: + try: + min_val = float(min_range) + max_val = float(max_range) + return numeric_value < min_val or numeric_value > max_val + except (ValueError, TypeError): + pass + + return False + + def _is_valid_status_transition(self, current_status: ResultStatus, new_status: ResultStatus) -> bool: + """Validate if status transition is allowed""" + valid_transitions = { + ResultStatus.PENDING_VERIFICATION: [ResultStatus.VERIFIED, ResultStatus.REJECTED], + ResultStatus.VERIFIED: [], # Final state + ResultStatus.REJECTED: [ResultStatus.PENDING_VERIFICATION] # Can be resubmitted + } + + return new_status in valid_transitions.get(current_status, []) \ No newline at end of file diff --git a/Lab-Scheduling/services/lab_schedule_service.py b/Lab-Scheduling/services/lab_schedule_service.py new file mode 100644 index 0000000..3419141 --- /dev/null +++ b/Lab-Scheduling/services/lab_schedule_service.py @@ -0,0 +1,291 @@ +""" +Lab Schedule Service +Business logic layer for lab schedule operations +""" + +from typing import List, Optional, Dict +from sqlalchemy.orm import Session +from datetime import datetime, date, time, timedelta + +from repositories.lab_schedule_repository import LabScheduleRepository +from repositories.lab_order_repository import LabOrderRepository +from models.lab_schedule import LabSchedule, ScheduleStatus, SampleType +from models.lab_order import OrderStatus +from exceptions import ( + LabScheduleNotFoundException, + LabOrderNotFoundException, + ScheduleConflictException, + InvalidScheduleTimeException, + LabSchedulingException, + DatabaseConnectionException +) + + +class LabScheduleService: + """Service for lab schedule business logic""" + + def __init__(self, db: Session): + self.db = db + self.repository = LabScheduleRepository(db) + self.order_repository = LabOrderRepository(db) + + def create_schedule(self, schedule_data: dict) -> LabSchedule: + """Create a new lab schedule with validation""" + try: + # Validate required fields + required_fields = ['order_id', 'technician_id', 'scheduled_datetime'] + for field in required_fields: + if field not in schedule_data or not schedule_data[field]: + raise LabSchedulingException(f"Missing required field: {field}") + + # Validate order exists and is in correct status + order = self.order_repository.get_by_id(schedule_data['order_id']) + if order.status != OrderStatus.PENDING: + raise LabSchedulingException(f"Cannot schedule order with status {order.status.value}") + + # Validate scheduling time + scheduled_datetime = schedule_data['scheduled_datetime'] + if isinstance(scheduled_datetime, str): + scheduled_datetime = datetime.fromisoformat(scheduled_datetime) + + if not self._is_valid_schedule_time(scheduled_datetime): + raise InvalidScheduleTimeException("Invalid scheduling time") + + # Check for conflicts + if self.repository.check_conflicts( + schedule_data['technician_id'], + scheduled_datetime + ): + raise ScheduleConflictException("Technician is not available at the requested time") + + # Set default values + schedule_data.setdefault('status', ScheduleStatus.SCHEDULED) + schedule_data.setdefault('sample_type', SampleType.BLOOD) + schedule_data.setdefault('is_home_collection', False) + schedule_data.setdefault('created_at', datetime.utcnow()) + schedule_data.setdefault('updated_at', datetime.utcnow()) + + # Create the schedule + schedule = self.repository.create(schedule_data) + + # Update order status to scheduled + self.order_repository.update_status(schedule_data['order_id'], OrderStatus.SCHEDULED) + + return schedule + + except (LabOrderNotFoundException, ScheduleConflictException, InvalidScheduleTimeException, LabSchedulingException): + raise + except Exception as e: + raise DatabaseConnectionException(f"Failed to create schedule: {str(e)}") + + def get_schedule(self, schedule_id: int) -> LabSchedule: + """Get lab schedule by ID""" + return self.repository.get_by_id(schedule_id) + + def get_schedule_by_order(self, order_id: int) -> Optional[LabSchedule]: + """Get schedule for a specific order""" + return self.repository.get_by_order_id(order_id) + + def get_technician_schedule(self, technician_id: int, schedule_date: date = None) -> List[LabSchedule]: + """Get schedules for a technician""" + return self.repository.get_by_technician_id(technician_id, schedule_date) + + def get_available_slots(self, schedule_date: date, technician_id: int = None) -> List[Dict]: + """Get available time slots for scheduling""" + return self.repository.get_available_slots(schedule_date, technician_id) + + def get_schedules_by_date_range(self, start_date: date, end_date: date) -> List[LabSchedule]: + """Get schedules within date range""" + return self.repository.get_by_date_range(start_date, end_date) + + def get_schedules_by_status(self, status: ScheduleStatus) -> List[LabSchedule]: + """Get schedules by status""" + return self.repository.get_by_status(status) + + def get_home_collections(self, schedule_date: date = None) -> List[LabSchedule]: + """Get home collection schedules""" + return self.repository.get_home_collections(schedule_date) + + def update_schedule_status(self, schedule_id: int, status: ScheduleStatus, notes: str = None) -> LabSchedule: + """Update schedule status with business logic validation""" + try: + # Get current schedule + schedule = self.repository.get_by_id(schedule_id) + + # Validate status transition + if not self._is_valid_status_transition(schedule.status, status): + raise LabSchedulingException( + f"Invalid status transition from {schedule.status.value} to {status.value}" + ) + + # Update schedule status + updated_schedule = self.repository.update_status(schedule_id, status) + + # Update related order status if needed + if status == ScheduleStatus.IN_PROGRESS: + self.order_repository.update_status(schedule.order_id, OrderStatus.IN_PROGRESS) + elif status == ScheduleStatus.COMPLETED: + self.order_repository.update_status(schedule.order_id, OrderStatus.COMPLETED) + elif status == ScheduleStatus.CANCELLED: + self.order_repository.update_status(schedule.order_id, OrderStatus.PENDING) + + # Add notes if provided + if notes: + self.repository.update(schedule_id, {'notes': notes}) + + return updated_schedule + + except LabScheduleNotFoundException: + raise + except LabSchedulingException: + raise + except Exception as e: + raise DatabaseConnectionException(f"Failed to update schedule status: {str(e)}") + + def reschedule_appointment(self, schedule_id: int, new_datetime: datetime, new_technician_id: int = None) -> LabSchedule: + """Reschedule an appointment""" + try: + # Get current schedule + schedule = self.repository.get_by_id(schedule_id) + + # Check if schedule can be rescheduled + if schedule.status not in [ScheduleStatus.SCHEDULED]: + raise LabSchedulingException(f"Cannot reschedule appointment with status {schedule.status.value}") + + # Validate new scheduling time + if not self._is_valid_schedule_time(new_datetime): + raise InvalidScheduleTimeException("Invalid rescheduling time") + + # Use existing technician if not specified + technician_id = new_technician_id or schedule.technician_id + + # Check for conflicts + if self.repository.check_conflicts(technician_id, new_datetime): + raise ScheduleConflictException("Technician is not available at the requested time") + + # Update schedule + update_data = { + 'scheduled_datetime': new_datetime, + 'technician_id': technician_id + } + + return self.repository.update(schedule_id, update_data) + + except (LabScheduleNotFoundException, InvalidScheduleTimeException, ScheduleConflictException, LabSchedulingException): + raise + except Exception as e: + raise DatabaseConnectionException(f"Failed to reschedule appointment: {str(e)}") + + def cancel_schedule(self, schedule_id: int, reason: str) -> LabSchedule: + """Cancel a lab schedule""" + try: + schedule = self.repository.get_by_id(schedule_id) + + # Check if schedule can be cancelled + if schedule.status in [ScheduleStatus.COMPLETED, ScheduleStatus.CANCELLED]: + raise LabSchedulingException(f"Cannot cancel schedule with status {schedule.status.value}") + + # Update status and add cancellation reason + update_data = { + 'status': ScheduleStatus.CANCELLED, + 'notes': f"CANCELLED: {reason}. Previous notes: {schedule.notes or ''}" + } + + updated_schedule = self.repository.update(schedule_id, update_data) + + # Update order status back to pending + self.order_repository.update_status(schedule.order_id, OrderStatus.PENDING) + + return updated_schedule + + except LabScheduleNotFoundException: + raise + except LabSchedulingException: + raise + except Exception as e: + raise DatabaseConnectionException(f"Failed to cancel schedule: {str(e)}") + + def update_schedule(self, schedule_id: int, update_data: dict) -> LabSchedule: + """Update lab schedule with validation""" + try: + # Validate status transition if status is being updated + if 'status' in update_data: + current_schedule = self.repository.get_by_id(schedule_id) + if not self._is_valid_status_transition(current_schedule.status, update_data['status']): + raise LabSchedulingException( + f"Invalid status transition from {current_schedule.status.value} to {update_data['status'].value}" + ) + + # Validate scheduling time if being updated + if 'scheduled_datetime' in update_data: + if not self._is_valid_schedule_time(update_data['scheduled_datetime']): + raise InvalidScheduleTimeException("Invalid scheduling time") + + return self.repository.update(schedule_id, update_data) + + except LabScheduleNotFoundException: + raise + except (InvalidScheduleTimeException, LabSchedulingException): + raise + except Exception as e: + raise DatabaseConnectionException(f"Failed to update schedule: {str(e)}") + + def delete_schedule(self, schedule_id: int) -> bool: + """Delete a lab schedule (only if not started)""" + try: + schedule = self.repository.get_by_id(schedule_id) + + # Check if schedule can be deleted + if schedule.status not in [ScheduleStatus.SCHEDULED, ScheduleStatus.CANCELLED]: + raise LabSchedulingException( + f"Cannot delete schedule with status {schedule.status.value}. Only scheduled or cancelled schedules can be deleted." + ) + + # Update order status back to pending if schedule was active + if schedule.status == ScheduleStatus.SCHEDULED: + self.order_repository.update_status(schedule.order_id, OrderStatus.PENDING) + + return self.repository.delete(schedule_id) + + except LabScheduleNotFoundException: + raise + except LabSchedulingException: + raise + except Exception as e: + raise DatabaseConnectionException(f"Failed to delete schedule: {str(e)}") + + def get_all_schedules(self, skip: int = 0, limit: int = 100) -> List[LabSchedule]: + """Get all schedules with pagination""" + return self.repository.get_all(skip, limit) + + def get_todays_schedules(self) -> List[LabSchedule]: + """Get today's schedules""" + today = date.today() + return self.repository.get_by_date_range(today, today) + + def _is_valid_schedule_time(self, scheduled_datetime: datetime) -> bool: + """Validate if scheduling time is valid""" + # Check if time is in the future + if scheduled_datetime <= datetime.now(): + return False + + # Check if time is within working hours (8 AM to 5 PM) + if scheduled_datetime.hour < 8 or scheduled_datetime.hour >= 17: + return False + + # Check if it's a weekday (Monday = 0, Sunday = 6) + if scheduled_datetime.weekday() >= 5: # Saturday or Sunday + return False + + return True + + def _is_valid_status_transition(self, current_status: ScheduleStatus, new_status: ScheduleStatus) -> bool: + """Validate if status transition is allowed""" + valid_transitions = { + ScheduleStatus.SCHEDULED: [ScheduleStatus.IN_PROGRESS, ScheduleStatus.CANCELLED], + ScheduleStatus.IN_PROGRESS: [ScheduleStatus.COMPLETED, ScheduleStatus.CANCELLED], + ScheduleStatus.COMPLETED: [], # Final state + ScheduleStatus.CANCELLED: [] # Final state + } + + return new_status in valid_transitions.get(current_status, []) \ No newline at end of file