# 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 Information

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 }}

{{ appointment.package_purchase.sessions_used }} / {{ appointment.package_purchase.total_sessions }}
{% endif %} ``` ### 5. Add Package Scheduling View Create a view to schedule all appointments for a package: ```python @login_required def schedule_package_view(request, package_purchase_id): """Schedule all appointments for a package purchase.""" from finance.models import PackagePurchase from .package_integration_service import PackageIntegrationService package_purchase = get_object_or_404( PackagePurchase, id=package_purchase_id, patient__tenant=request.user.tenant ) if request.method == 'POST': # Get form data provider_id = request.POST.get('provider') start_date = request.POST.get('start_date') preferred_days = request.POST.getlist('preferred_days') # Schedule appointments appointments, errors = PackageIntegrationService.schedule_package_appointments( package_purchase=package_purchase, provider_id=provider_id, start_date=date.fromisoformat(start_date), preferred_days=[int(d) for d in preferred_days] if preferred_days else None, auto_schedule=True ) if errors: messages.warning(request, f"Scheduled {len(appointments)} appointments with some errors") else: messages.success(request, f"Successfully scheduled {len(appointments)} appointments") return redirect('finance:package_purchase_detail', pk=package_purchase.id) # Show form return render(request, 'appointments/schedule_package_form.html', { 'package_purchase': package_purchase }) ``` ## Files Modified ### Modified Files 1. `appointments/models.py` - Added package_purchase and session_number_in_package fields 2. `appointments/admin.py` - Added package_info display method 3. `appointments/signals.py` - Added package usage increment on completion ### New Files 1. `appointments/package_integration_service.py` - Integration service 2. `appointments/migrations/0006_*.py` - Database migration 3. `PACKAGE_APPOINTMENTS_FINANCE_INTEGRATION.md` - This document ### Removed Files 1. `appointments/package_models.py` - Replaced by finance models 2. `appointments/package_forms.py` - Simplified to use finance packages 3. `appointments/package_scheduling_service.py` - Replaced by integration service ## Integration Points ### Finance App → Appointments App 1. **Package Purchase Created**: - User can schedule appointments from finance app - Link to appointment scheduling in package purchase detail 2. **Package Services**: - Defines which services are included - Determines clinic, duration, service type - Used by auto-scheduling 3. **Package Expiry**: - Enforced by finance.PackagePurchase - Cannot schedule appointments after expiry ### Appointments App → Finance App 1. **Appointment Completion**: - Increments sessions_used on PackagePurchase - Updates package status when complete 2. **Appointment Cancellation**: - Does NOT decrement sessions_used (policy decision) - Cancelled appointments still count as used 3. **Progress Tracking**: - Appointments show package progress - Admin shows package info ## User Workflows ### Workflow 1: Purchase Package → Schedule Appointments 1. Patient purchases package in finance app 2. PackagePurchase created with status='ACTIVE' 3. User clicks "Schedule Appointments" button 4. Redirected to appointment scheduling form 5. Selects provider, start date, preferred days 6. System auto-schedules all sessions 7. Appointments created and linked to package ### Workflow 2: Book Single Appointment from Package 1. User creates new appointment 2. Selects patient 3. Form shows available packages for patient 4. User selects package (optional) 5. Appointment created and linked to package 6. Package sessions_used NOT incremented yet 7. When appointment completed → sessions_used++ ### Workflow 3: View Package Progress 1. User views PackagePurchase in finance app 2. Sees list of all appointments 3. Progress bar shows completion 4. Can click appointments to view details ## Testing Checklist ### Single Session Appointments - [ ] Create appointment without package - [ ] Verify package_purchase is NULL - [ ] Complete appointment - [ ] Verify no package updates ### Package-Based Appointments - [ ] Purchase a package in finance - [ ] Schedule appointments using integration service - [ ] Verify all appointments created - [ ] Verify package_purchase links correct - [ ] Verify session numbers sequential - [ ] Complete first appointment - [ ] Verify sessions_used incremented - [ ] Complete all appointments - [ ] Verify package status = 'COMPLETED' ### Auto-Scheduling - [ ] Schedule with preferred days - [ ] Verify only scheduled on those days - [ ] Schedule with no preferred days - [ ] Verify scheduled on any available day - [ ] Schedule with limited availability - [ ] Verify error handling ### Multiple Providers - [ ] Schedule with different providers per session - [ ] Verify each appointment has correct provider - [ ] Verify availability checked per provider ### Admin Interface - [ ] View appointment with package in admin - [ ] Verify package info displayed - [ ] Filter appointments by package - [ ] View package purchase with appointments ## API Integration (Future) Consider adding REST API endpoints: ```python # GET /api/v1/packages/available/?patient=&clinic= # Returns available packages for patient # POST /api/v1/packages//schedule/ # Schedules all appointments for a package # GET /api/v1/packages//progress/ # Returns package progress information ``` ## Performance Considerations 1. **Queries**: Use `select_related('package_purchase__package')` when fetching appointments 2. **Indexing**: Added index on package_purchase field 3. **Caching**: Consider caching package progress calculations 4. **Bulk Operations**: Use bulk_create for scheduling multiple appointments ## Security 1. **Tenant Isolation**: All queries filter by tenant 2. **Permission Checks**: Only authorized users can schedule packages 3. **Package Ownership**: Verify patient owns package before scheduling 4. **Expiry Validation**: Check package not expired before scheduling ## Conclusion The appointments system now seamlessly integrates with the finance package system. This provides a unified experience for managing service packages while avoiding code duplication and maintaining data integrity. ## Support For questions or issues: - Review finance.models.Package and finance.models.PackagePurchase - Check appointments.package_integration_service for scheduling logic - See appointments.signals for automatic package usage tracking - All code includes comprehensive docstrings and comments