Files
smoothschedule/smoothschedule/CALENDAR_SYNC_INTEGRATION.md
poduck e4ad7fca87 feat: Plan-based feature permissions and quota enforcement
Backend:
- Add HasQuota() permission factory for quota limits (resources, users, services, appointments, email templates, automated tasks)
- Add HasFeaturePermission() factory for feature-based permissions (SMS, masked calling, custom domains, white label, plugins, webhooks, calendar sync, analytics)
- Add has_feature() method to Tenant model for flexible permission checking
- Add new tenant permission fields: can_create_plugins, can_use_webhooks, can_use_calendar_sync, can_export_data
- Create Data Export API with CSV/JSON support for appointments, customers, resources, services
- Create Analytics API with dashboard, appointments, revenue endpoints
- Add calendar sync views and URL configuration

Frontend:
- Add usePlanFeatures hook for checking feature availability
- Add UpgradePrompt components (inline, banner, overlay variants)
- Add LockedSection wrapper and LockedButton for feature gating
- Update settings pages with permission checks

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-02 11:21:11 -05:00

342 lines
9.4 KiB
Markdown

# Calendar Sync Feature Integration Guide
This document explains how the calendar sync feature permission system works and provides examples for implementation.
## Overview
The calendar sync feature allows tenants to:
1. Connect to Google Calendar or Outlook Calendar via OAuth
2. Sync calendar events into the scheduling system
3. Manage multiple calendar integrations
The feature is gated by the `can_use_calendar_sync` permission that must be enabled at the tenant level.
## Architecture
### 1. Database Model (Tenant)
Added to `/home/poduck/Desktop/smoothschedule2/smoothschedule/core/models.py`:
```python
can_use_calendar_sync = models.BooleanField(
default=False,
help_text="Whether this business can sync Google Calendar and other calendar providers"
)
```
### 2. Permission Check (HasFeaturePermission)
Updated in `/home/poduck/Desktop/smoothschedule2/smoothschedule/core/permissions.py`:
```python
FEATURE_NAMES = {
# ... other features ...
'can_use_calendar_sync': 'Calendar Sync',
}
```
The `HasFeaturePermission` factory function creates a DRF permission class:
```python
from core.permissions import HasFeaturePermission
permission_classes = [IsAuthenticated, HasFeaturePermission('can_use_calendar_sync')]
```
### 3. OAuth Integration
Modified in `/home/poduck/Desktop/smoothschedule2/smoothschedule/core/oauth_views.py`:
- `GoogleOAuthInitiateView`: Checks permission when purpose is "calendar"
- `MicrosoftOAuthInitiateView`: Checks permission when purpose is "calendar"
```python
def post(self, request):
purpose = request.data.get('purpose', 'email')
# Check calendar sync permission if purpose is calendar
if purpose == 'calendar':
calendar_permission = HasFeaturePermission('can_use_calendar_sync')
if not calendar_permission().has_permission(request, self):
return Response({
'success': False,
'error': 'Your current plan does not include Calendar Sync.',
}, status=status.HTTP_403_FORBIDDEN)
# Continue with OAuth flow...
```
### 4. Calendar Sync Views
Created in `/home/poduck/Desktop/smoothschedule2/smoothschedule/schedule/calendar_sync_views.py`:
- `CalendarListView`: Lists connected calendars
- `CalendarSyncView`: Syncs calendar events
- `CalendarDeleteView`: Disconnects calendar integration
- `CalendarStatusView`: Gets calendar sync status
#### Permission Pattern in Views
```python
from core.permissions import HasFeaturePermission
from rest_framework.permissions import IsAuthenticated
class CalendarSyncPermission(IsAuthenticated):
"""Custom permission combining auth + feature check"""
def has_permission(self, request, view):
if not super().has_permission(request, view):
return False
tenant = getattr(request, 'tenant', None)
if not tenant:
return False
return tenant.has_feature('can_use_calendar_sync')
class CalendarListView(APIView):
permission_classes = [CalendarSyncPermission]
def get(self, request):
# This endpoint is only accessible if:
# 1. User is authenticated
# 2. Tenant has can_use_calendar_sync enabled
...
```
## Usage Examples
### 1. Enable Calendar Sync for a Tenant
```python
# Via Django shell or management command
from core.models import Tenant
tenant = Tenant.objects.get(schema_name='demo')
tenant.can_use_calendar_sync = True
tenant.save()
# Check if tenant has feature
if tenant.has_feature('can_use_calendar_sync'):
print("Calendar sync enabled!")
```
### 2. API Endpoints
#### Initiate Google Calendar OAuth
```bash
POST /api/oauth/google/initiate/
Content-Type: application/json
{
"purpose": "calendar"
}
# Response (if permission granted):
{
"success": true,
"authorization_url": "https://accounts.google.com/o/oauth2/auth?..."
}
# Response (if permission denied):
{
"success": false,
"error": "Your current plan does not include Calendar Sync. Please upgrade..."
}
```
#### List Connected Calendars
```bash
GET /api/calendar/list/
# Response:
{
"success": true,
"calendars": [
{
"id": 1,
"provider": "Google",
"email": "user@gmail.com",
"is_valid": true,
"is_expired": false,
"last_used_at": "2025-12-02T10:30:00Z",
"created_at": "2025-12-01T08:15:00Z"
}
]
}
```
#### Sync Calendar Events
```bash
POST /api/calendar/sync/
Content-Type: application/json
{
"credential_id": 1,
"calendar_id": "primary",
"start_date": "2025-12-01",
"end_date": "2025-12-31"
}
# Response:
{
"success": true,
"message": "Calendar sync started for user@gmail.com"
}
```
#### Check Calendar Sync Status
```bash
GET /api/calendar/status/
# Response (if feature enabled):
{
"success": true,
"can_use_calendar_sync": true,
"total_connected": 2,
"feature_enabled": true
}
# Response (if feature not enabled):
{
"success": true,
"can_use_calendar_sync": false,
"message": "Calendar Sync feature is not available for your plan",
"total_connected": 0
}
```
## Permission Flow Diagram
```
User Request to Calendar Endpoint
[Is User Authenticated?]
├─ NO → 401 Unauthorized
└─ YES ↓
[Is Request in Tenant Context?]
├─ NO → 400 Bad Request
└─ YES ↓
[Does Tenant have can_use_calendar_sync?]
├─ NO → 403 Permission Denied (upgrade message)
└─ YES ↓
[Process Request]
├─ Success → 200 OK
└─ Error → 500 Server Error
```
## Integration with ViewSets
For ModelViewSet endpoints (like scheduling events from calendar sync):
```python
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from core.permissions import HasFeaturePermission
from schedule.models import Event
class EventViewSet(viewsets.ModelViewSet):
queryset = Event.objects.all()
serializer_class = EventSerializer
# Permission for create/update/delete operations
permission_classes = [IsAuthenticated, HasFeaturePermission('can_use_calendar_sync')]
def get_queryset(self):
# Only show events that user's tenant can access
return Event.objects.filter(tenant=self.request.tenant)
@action(detail=False, methods=['post'])
def sync_from_calendar(self, request):
"""Create events from a calendar sync"""
# This action is protected by the permission_classes
calendar_id = request.data.get('calendar_id')
# ... implement sync logic ...
```
Note: If you only want to gate specific actions, override `get_permissions()`:
```python
def get_permissions(self):
if self.action in ['create', 'update', 'destroy', 'sync_from_calendar']:
# Only allow these actions if calendar sync is enabled
return [IsAuthenticated(), HasFeaturePermission('can_use_calendar_sync')()]
# Read-only actions don't require calendar sync permission
return [IsAuthenticated()]
```
## Migration
The migration `0016_tenant_can_use_calendar_sync.py` adds the field to existing tenants with default value of `False`.
To apply the migration:
```bash
cd /home/poduck/Desktop/smoothschedule2/smoothschedule
docker compose -f docker-compose.local.yml exec django python manage.py migrate
```
## Testing
### Test Permission Denied
```python
from django.test import TestCase, RequestFactory
from rest_framework.test import APITestCase
from core.models import Tenant
from smoothschedule.users.models import User
class CalendarSyncTests(APITestCase):
def setUp(self):
self.tenant = Tenant.objects.create(
schema_name='test',
name='Test Tenant',
can_use_calendar_sync=False # Feature disabled
)
self.user = User.objects.create_user(
email='user@test.com',
password='testpass',
tenant=self.tenant
)
self.client.force_authenticate(user=self.user)
def test_calendar_sync_permission_denied(self):
"""Test that users without permission cannot access calendar sync"""
response = self.client.post('/api/calendar/list/')
# Should return 403 Forbidden with upgrade message
self.assertEqual(response.status_code, 403)
self.assertIn('upgrade', response.json()['error'].lower())
def test_calendar_sync_permission_granted(self):
"""Test that users with permission can access calendar sync"""
self.tenant.can_use_calendar_sync = True
self.tenant.save()
response = self.client.get('/api/calendar/list/')
# Should return 200 OK
self.assertEqual(response.status_code, 200)
```
## Security Considerations
1. **Permission is checked at view level**: The `CalendarSyncPermission` checks that the tenant has the feature enabled
2. **Tenant isolation**: OAuthCredential queries filter by tenant to ensure data isolation
3. **Token security**: Tokens are stored encrypted at rest (configured in settings)
4. **CSRF protection**: OAuth state parameter prevents CSRF attacks
5. **Audit logging**: All calendar sync operations are logged with tenant and user information
## Related Files
- Model: `/home/poduck/Desktop/smoothschedule2/smoothschedule/core/models.py` (Tenant.can_use_calendar_sync)
- Permission: `/home/poduck/Desktop/smoothschedule2/smoothschedule/core/permissions.py` (HasFeaturePermission)
- OAuth Views: `/home/poduck/Desktop/smoothschedule2/smoothschedule/core/oauth_views.py`
- Calendar Views: `/home/poduck/Desktop/smoothschedule2/smoothschedule/schedule/calendar_sync_views.py`
- Migration: `/home/poduck/Desktop/smoothschedule2/smoothschedule/core/migrations/0016_tenant_can_use_calendar_sync.py`