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_usedon 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_purchasefield to Appointment - Adds
session_number_in_packagefield to Appointment - Updates historical tables
How It Works
Finance Package System (Existing)
-
Package Definition (
finance.Package):- Name, description, price
- Contains multiple services via
PackageService - Total sessions calculated from services
- Validity period in days
-
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)
-
Single Session Appointment:
package_purchase= NULLsession_number_in_package= NULL- Works exactly as before
-
Package-Based Appointment:
package_purchase= Link to PackagePurchasesession_number_in_package= 1, 2, 3, etc.- Auto-scheduled based on availability
- Increments package usage on completion
Workflow
Patient Purchases Package (Finance App):
- Patient selects a package (e.g., "10 SLP Sessions")
- Invoice created and paid
- PackagePurchase record created with:
- total_sessions = 10
- sessions_used = 0
- expiry_date = purchase_date + validity_days
Scheduling Package Appointments (Appointments App):
- User initiates package scheduling
- Selects provider(s) for sessions
- Sets preferred days and date range
- System calls
PackageIntegrationService.schedule_package_appointments() - 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:
- Patient attends appointment
- Appointment status → COMPLETED
- Signal triggers
increment_package_usage() - PackagePurchase.sessions_used += 1
- 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_purchaseis NULL - Package appointments:
package_purchaselinks to PackagePurchase - No changes to existing single appointment workflow
2. Multiple Provider Support
provider_assignmentsdict 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_usedincremented automaticallysessions_remainingcalculated 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
- No Duplication: Single package system
- Billing Integration: Packages tied to invoices
- Financial Tracking: Revenue and usage in one place
- Simpler Code: Fewer models to maintain
- Better UX: Consistent package experience
- 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
appointments/models.py- Added package_purchase and session_number_in_package fieldsappointments/admin.py- Added package_info display methodappointments/signals.py- Added package usage increment on completion
New Files
appointments/package_integration_service.py- Integration serviceappointments/migrations/0006_*.py- Database migrationPACKAGE_APPOINTMENTS_FINANCE_INTEGRATION.md- This document
Removed Files
appointments/package_models.py- Replaced by finance modelsappointments/package_forms.py- Simplified to use finance packagesappointments/package_scheduling_service.py- Replaced by integration service
Integration Points
Finance App → Appointments App
-
Package Purchase Created:
- User can schedule appointments from finance app
- Link to appointment scheduling in package purchase detail
-
Package Services:
- Defines which services are included
- Determines clinic, duration, service type
- Used by auto-scheduling
-
Package Expiry:
- Enforced by finance.PackagePurchase
- Cannot schedule appointments after expiry
Appointments App → Finance App
-
Appointment Completion:
- Increments sessions_used on PackagePurchase
- Updates package status when complete
-
Appointment Cancellation:
- Does NOT decrement sessions_used (policy decision)
- Cancelled appointments still count as used
-
Progress Tracking:
- Appointments show package progress
- Admin shows package info
User Workflows
Workflow 1: Purchase Package → Schedule Appointments
- Patient purchases package in finance app
- PackagePurchase created with status='ACTIVE'
- User clicks "Schedule Appointments" button
- Redirected to appointment scheduling form
- Selects provider, start date, preferred days
- System auto-schedules all sessions
- Appointments created and linked to package
Workflow 2: Book Single Appointment from Package
- User creates new appointment
- Selects patient
- Form shows available packages for patient
- User selects package (optional)
- Appointment created and linked to package
- Package sessions_used NOT incremented yet
- When appointment completed → sessions_used++
Workflow 3: View Package Progress
- User views PackagePurchase in finance app
- Sees list of all appointments
- Progress bar shows completion
- 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
- Queries: Use
select_related('package_purchase__package')when fetching appointments - Indexing: Added index on package_purchase field
- Caching: Consider caching package progress calculations
- Bulk Operations: Use bulk_create for scheduling multiple appointments
Security
- Tenant Isolation: All queries filter by tenant
- Permission Checks: Only authorized users can schedule packages
- Package Ownership: Verify patient owns package before scheduling
- 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