agdar/PACKAGE_APPOINTMENTS_FINANCE_INTEGRATION.md
Marwan Alwali 7e014ee160 update
2025-11-16 14:56:32 +03:00

18 KiB

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:

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

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

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

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

python3 manage.py migrate appointments

2. Update Appointment Form

Modify appointments/forms.py to add package selection:

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:

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:

{% if appointment.package_purchase %}
<div class="card mb-3">
    <div class="card-header">
        <h5><i class="fas fa-box me-2"></i>Package Information</h5>
    </div>
    <div class="card-body">
        <p><strong>Package:</strong> {{ appointment.package_purchase.package.name_en }}</p>
        <p><strong>Session:</strong> {{ appointment.session_number_in_package }} of {{ appointment.package_purchase.total_sessions }}</p>
        <p><strong>Sessions Remaining:</strong> {{ appointment.package_purchase.sessions_remaining }}</p>
        <p><strong>Expiry Date:</strong> {{ appointment.package_purchase.expiry_date }}</p>
        
        <div class="progress">
            <div class="progress-bar" role="progressbar" 
                 style="width: {{ appointment.package_purchase.sessions_used|mul:100|div:appointment.package_purchase.total_sessions }}%">
                {{ appointment.package_purchase.sessions_used }} / {{ appointment.package_purchase.total_sessions }}
            </div>
        </div>
    </div>
</div>
{% endif %}

5. Add Package Scheduling View

Create a view to schedule all appointments for a package:

@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:

# GET /api/v1/packages/available/?patient=<id>&clinic=<id>
# Returns available packages for patient

# POST /api/v1/packages/<id>/schedule/
# Schedules all appointments for a package

# GET /api/v1/packages/<id>/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