577 lines
18 KiB
Markdown
577 lines
18 KiB
Markdown
# 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 %}
|
|
<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:
|
|
|
|
```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=<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
|