# Package Appointments - Finance Integration Implementation ## Overview This document describes the integration of the appointments system with the existing finance package system. Instead of creating duplicate package models, appointments now integrate directly with `finance.Package` and `finance.PackagePurchase`. ## Implementation Date November 11, 2025 ## Architecture Decision **Decision**: Integrate with existing finance package system (Option A) **Rationale**: - Avoid code duplication - Single source of truth for packages - Automatic billing integration - Simpler architecture - Better user experience ## What Was Implemented ### 1. **Appointment Model Updates** (`appointments/models.py`) Added two new fields to the `Appointment` model: ```python # Package Integration (links to finance.PackagePurchase) package_purchase = models.ForeignKey( 'finance.PackagePurchase', on_delete=models.SET_NULL, null=True, blank=True, related_name='appointments', verbose_name=_("Package Purchase"), help_text=_("Link to package purchase if this appointment is part of a package") ) session_number_in_package = models.PositiveIntegerField( null=True, blank=True, verbose_name=_("Session Number in Package"), help_text=_("Session number within the package (1, 2, 3, ...)") ) ``` **Benefits:** - Links appointments to purchased packages - Tracks session order within package - Enables package progress tracking - Supports package-based billing ### 2. **Package Integration Service** (`appointments/package_integration_service.py`) Created `PackageIntegrationService` class with the following methods: #### **schedule_package_appointments()** - Schedules all appointments for a purchased package - Supports single or multiple providers - Respects preferred days and date range - Uses existing availability service - Returns list of created appointments and errors #### **increment_package_usage()** - Increments `sessions_used` on PackagePurchase when appointment completed - Updates package status to 'COMPLETED' when all sessions used - Called automatically via signal #### **get_available_packages_for_patient()** - Returns active packages for a patient with remaining sessions - Filters by clinic if provided - Used in appointment creation form #### **get_package_progress()** - Returns comprehensive progress information - Lists all appointments in package - Shows scheduled, completed, cancelled counts #### Helper Methods: - `_get_clinic_from_package()`: Extracts clinic from package services - `_get_service_type_from_package()`: Gets service type from package name - `_get_duration_from_package()`: Gets session duration from package services - `_generate_appointment_number()`: Generates unique appointment numbers ### 3. **Signal Integration** (`appointments/signals.py`) Updated `handle_appointment_completed()` to: - Automatically increment package usage when appointment is completed - Log package progress - Handle errors gracefully ```python if appointment.package_purchase: PackageIntegrationService.increment_package_usage(appointment) ``` ### 4. **Admin Interface Updates** (`appointments/admin.py`) Updated `AppointmentAdmin` to: - Show package information in list display - Add package fields to fieldsets - Display package name and session progress ```python def package_info(self, obj): """Display package information if appointment is part of a package.""" if obj.package_purchase: return f"{obj.package_purchase.package.name_en} (Session {obj.session_number_in_package}/{obj.package_purchase.total_sessions})" return "-" ``` ### 5. **Database Migration** Created migration `0006_remove_packagesession_package_and_more.py`: - Removes old package models (AppointmentPackage, PackageSession, ProviderAssignment) - Adds `package_purchase` field to Appointment - Adds `session_number_in_package` field to Appointment - Updates historical tables ## How It Works ### Finance Package System (Existing) 1. **Package Definition** (`finance.Package`): - Name, description, price - Contains multiple services via `PackageService` - Total sessions calculated from services - Validity period in days 2. **Package Purchase** (`finance.PackagePurchase`): - Patient purchases a package - Tracks total sessions and sessions used - Has expiry date - Status: ACTIVE, EXPIRED, COMPLETED, CANCELLED ### Appointment Integration (New) 1. **Single Session Appointment**: - `package_purchase` = NULL - `session_number_in_package` = NULL - Works exactly as before 2. **Package-Based Appointment**: - `package_purchase` = Link to PackagePurchase - `session_number_in_package` = 1, 2, 3, etc. - Auto-scheduled based on availability - Increments package usage on completion ### Workflow #### Patient Purchases Package (Finance App): 1. Patient selects a package (e.g., "10 SLP Sessions") 2. Invoice created and paid 3. PackagePurchase record created with: - total_sessions = 10 - sessions_used = 0 - expiry_date = purchase_date + validity_days #### Scheduling Package Appointments (Appointments App): 1. User initiates package scheduling 2. Selects provider(s) for sessions 3. Sets preferred days and date range 4. System calls `PackageIntegrationService.schedule_package_appointments()` 5. Service creates appointments: - Links each to package_purchase - Sets session_number_in_package (1, 2, 3, ...) - Finds available slots based on preferences - Creates appointment records #### Completing Package Appointments: 1. Patient attends appointment 2. Appointment status → COMPLETED 3. Signal triggers `increment_package_usage()` 4. PackagePurchase.sessions_used += 1 5. If sessions_used == total_sessions: - PackagePurchase.status = 'COMPLETED' ## Database Schema ### Existing (Finance App) ``` Package (1) -----> (N) PackageService -----> (N) Service Package (1) -----> (N) PackagePurchase PackagePurchase (N) -----> (1) Patient PackagePurchase (N) -----> (1) Invoice ``` ### New Integration ``` PackagePurchase (1) -----> (N) Appointment Appointment.package_purchase → PackagePurchase Appointment.session_number_in_package → Integer (1, 2, 3, ...) ``` ## Key Features ### 1. **Single vs Package Differentiation** - Single appointments: `package_purchase` is NULL - Package appointments: `package_purchase` links to PackagePurchase - No changes to existing single appointment workflow ### 2. **Multiple Provider Support** - `provider_assignments` dict maps session numbers to provider IDs - Each appointment can have different provider - Respects provider availability for each session ### 3. **Auto-Scheduling** - Finds available slots based on provider schedules - Respects preferred days (Sunday-Saturday) - Schedules sessions sequentially with 1+ day gap - Tries up to 90 days to find slots - Returns errors for failed sessions ### 4. **Preferred Days** - User selects specific days of week - System only schedules on those days - Empty list = any day acceptable ### 5. **Progress Tracking** - `sessions_used` incremented automatically - `sessions_remaining` calculated property - Package status updated when complete - All appointments visible in admin ### 6. **Package Expiry** - Handled by finance.PackagePurchase - Expiry date enforced - Expired packages cannot be used ## Benefits of Integration 1. **No Duplication**: Single package system 2. **Billing Integration**: Packages tied to invoices 3. **Financial Tracking**: Revenue and usage in one place 4. **Simpler Code**: Fewer models to maintain 5. **Better UX**: Consistent package experience 6. **Audit Trail**: Complete history via simple-history ## Usage Examples ### Schedule Package Appointments ```python from appointments.package_integration_service import PackageIntegrationService from finance.models import PackagePurchase # Get patient's package purchase package_purchase = PackagePurchase.objects.get(id=package_id) # Schedule appointments appointments, errors = PackageIntegrationService.schedule_package_appointments( package_purchase=package_purchase, provider_id=provider_id, start_date=date.today(), end_date=None, # Uses package expiry date preferred_days=[0, 2, 4], # Sunday, Tuesday, Thursday use_multiple_providers=False, provider_assignments=None, auto_schedule=True ) # Check results print(f"Scheduled {len(appointments)} appointments") if errors: print(f"Errors: {errors}") ``` ### Get Available Packages for Patient ```python from appointments.package_integration_service import PackageIntegrationService # Get available packages packages = PackageIntegrationService.get_available_packages_for_patient( patient=patient, clinic=clinic # Optional filter ) for pkg in packages: print(f"{pkg.package.name_en}: {pkg.sessions_remaining} sessions remaining") ``` ### Track Package Progress ```python from appointments.package_integration_service import PackageIntegrationService # Get progress progress = PackageIntegrationService.get_package_progress(package_purchase) print(f"Total: {progress['total_sessions']}") print(f"Used: {progress['sessions_used']}") print(f"Remaining: {progress['sessions_remaining']}") print(f"Scheduled: {progress['scheduled_appointments']}") print(f"Completed: {progress['completed_appointments']}") ``` ## Next Steps ### 1. Run Migration ```bash python3 manage.py migrate appointments ``` ### 2. Update Appointment Form Modify `appointments/forms.py` to add package selection: ```python class AppointmentBookingForm(forms.ModelForm): # Add package selection field package_purchase = forms.ModelChoiceField( queryset=None, # Set in __init__ required=False, label=_('Use Package'), help_text=_('Select a package to use for this appointment') ) def __init__(self, *args, **kwargs): patient = kwargs.pop('patient', None) super().__init__(*args, **kwargs) if patient: # Show available packages for this patient from .package_integration_service import PackageIntegrationService self.fields['package_purchase'].queryset = \ PackageIntegrationService.get_available_packages_for_patient(patient) ``` ### 3. Update Appointment Creation View Modify `AppointmentCreateView` to handle package selection: ```python def form_valid(self, form): package_purchase = form.cleaned_data.get('package_purchase') if package_purchase: # Using a package if package_purchase.sessions_remaining <= 0: messages.error(self.request, 'No sessions remaining in package') return self.form_invalid(form) # Set package fields form.instance.package_purchase = package_purchase form.instance.session_number_in_package = package_purchase.sessions_used + 1 return super().form_valid(form) ``` ### 4. Update Appointment Detail Template Show package information in `appointments/templates/appointments/appointment_detail.html`: ```html {% if appointment.package_purchase %}
Package: {{ appointment.package_purchase.package.name_en }}
Session: {{ appointment.session_number_in_package }} of {{ appointment.package_purchase.total_sessions }}
Sessions Remaining: {{ appointment.package_purchase.sessions_remaining }}
Expiry Date: {{ appointment.package_purchase.expiry_date }}