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

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