# SmoothSchedule - Multi-Tenant Scheduling Platform ## Quick Reference ### Project Structure ``` /home/poduck/Desktop/smoothschedule2/ ├── frontend/ # React + Vite + TypeScript frontend │ └── CLAUDE.md # Frontend-specific docs │ ├── smoothschedule/ # Django backend (RUNS IN DOCKER!) │ └── CLAUDE.md # Backend-specific docs │ └── legacy_reference/ # Old code for reference (do not modify) ``` ### Development URLs - **Frontend:** `http://demo.lvh.me:5173` (or any business subdomain) - **Platform Frontend:** `http://platform.lvh.me:5173` - **Backend API:** `http://lvh.me:8000/api/` Note: `lvh.me` resolves to `127.0.0.1` - required for subdomain cookies to work. ## CRITICAL: Test-Driven Development (TDD) Required **All code changes MUST follow TDD.** This is non-negotiable. ### TDD Workflow 1. **Write tests FIRST** before writing any implementation code 2. **Run tests** to verify they fail (red) 3. **Write minimal code** to make tests pass (green) 4. **Refactor** while keeping tests green 5. **Repeat** for each new feature or bug fix ### Coverage Requirements | Target | Minimum | Goal | |--------|---------|------| | Backend (Django) | **80%** | 100% | | Frontend (React) | **80%** | 100% | ### Running Tests with Coverage **Backend (Django):** ```bash cd /home/poduck/Desktop/smoothschedule2/smoothschedule # Run all tests with coverage docker compose -f docker-compose.local.yml exec django pytest --cov --cov-report=term-missing # Run tests for a specific app docker compose -f docker-compose.local.yml exec django pytest smoothschedule/scheduling/schedule/tests/ --cov=smoothschedule/scheduling/schedule # Run a single test file docker compose -f docker-compose.local.yml exec django pytest smoothschedule/path/to/test_file.py -v # Run tests matching a pattern docker compose -f docker-compose.local.yml exec django pytest -k "test_create_resource" -v ``` **Frontend (React):** ```bash cd /home/poduck/Desktop/smoothschedule2/frontend # Run all tests with coverage npm test -- --coverage # Run tests in watch mode during development npm test # Run a single test file npm test -- src/hooks/__tests__/useResources.test.ts # Run tests matching a pattern npm test -- -t "should create resource" ``` ### Test File Organization **Backend:** ``` smoothschedule/smoothschedule/{domain}/{app}/ ├── models.py ├── views.py ├── serializers.py └── tests/ ├── __init__.py ├── test_models.py # Model unit tests ├── test_serializers.py # Serializer tests ├── test_views.py # API endpoint tests └── factories.py # Test factories (optional) ``` **Frontend:** ``` frontend/src/ ├── hooks/ │ ├── useResources.ts │ └── __tests__/ │ └── useResources.test.ts ├── components/ │ ├── MyComponent.tsx │ └── __tests__/ │ └── MyComponent.test.tsx └── pages/ ├── MyPage.tsx └── __tests__/ └── MyPage.test.tsx ``` ### What to Test **Backend:** - Model methods and properties - Model validation (clean methods) - Serializer validation - API endpoints (all HTTP methods) - Permission classes - Custom querysets and managers - Signals - Celery tasks - Utility functions **Frontend:** - Custom hooks (state changes, API calls) - Component rendering - User interactions (clicks, form submissions) - Conditional rendering - Error states - Loading states - API client functions ### TDD Example - Adding a New Feature **Step 1: Write the test first** ```python # Backend: test_views.py def test_create_resource_with_schedule(self, api_client, tenant): """New feature: resources can have a default schedule.""" data = { "name": "Test Resource", "type": "STAFF", "default_schedule": { "monday": {"start": "09:00", "end": "17:00"}, "tuesday": {"start": "09:00", "end": "17:00"}, } } response = api_client.post("/api/resources/", data, format="json") assert response.status_code == 201 assert response.data["default_schedule"]["monday"]["start"] == "09:00" ``` ```typescript // Frontend: useResources.test.ts it('should create resource with schedule', async () => { const { result } = renderHook(() => useCreateResource()); await act(async () => { await result.current.mutateAsync({ name: 'Test Resource', type: 'STAFF', defaultSchedule: { monday: { start: '09:00', end: '17:00' } } }); }); expect(mockApiClient.post).toHaveBeenCalledWith('/resources/', expect.objectContaining({ default_schedule: expect.any(Object) })); }); ``` **Step 2: Run tests - they should FAIL** **Step 3: Write minimal implementation to make tests pass** **Step 4: Refactor if needed while keeping tests green** ### Pre-Commit Checklist Before committing ANY code: 1. [ ] Tests written BEFORE implementation 2. [ ] All tests pass 3. [ ] Coverage meets minimum threshold (80%) 4. [ ] No skipped or disabled tests without justification ## CRITICAL: Backend Runs in Docker **NEVER run Django commands directly.** Always use Docker Compose: ```bash cd /home/poduck/Desktop/smoothschedule2/smoothschedule # Run migrations docker compose -f docker-compose.local.yml exec django python manage.py migrate # Django shell docker compose -f docker-compose.local.yml exec django python manage.py shell # View logs docker compose -f docker-compose.local.yml logs -f django # Any management command docker compose -f docker-compose.local.yml exec django python manage.py ``` ## Key Configuration Files ### Backend (Django) | File | Purpose | |------|---------| | `smoothschedule/docker-compose.local.yml` | Docker services config | | `smoothschedule/.envs/.local/.django` | Django env vars (SECRET_KEY, etc.) | | `smoothschedule/.envs/.local/.postgres` | Database credentials | | `smoothschedule/config/settings/local.py` | Local Django settings | | `smoothschedule/config/settings/base.py` | Base Django settings | | `smoothschedule/config/urls.py` | URL routing | ### Frontend (React) | File | Purpose | |------|---------| | `frontend/.env.development` | Vite env vars | | `frontend/vite.config.ts` | Vite configuration | | `frontend/src/api/client.ts` | Axios API client | | `frontend/src/types.ts` | TypeScript interfaces | | `frontend/src/i18n/locales/en.json` | Translations | | `frontend/src/utils/dateUtils.ts` | Date formatting utilities | ## Timezone Architecture (CRITICAL) All date/time handling follows this architecture to ensure consistency across timezones. ### Core Principles 1. **Database**: All times stored in UTC 2. **API Communication**: Always use UTC (both directions) 3. **API Responses**: Include `business_timezone` field 4. **Frontend Display**: Convert UTC based on `business_timezone` - If `business_timezone` is set → display in that timezone - If `business_timezone` is null/blank → display in user's local timezone ### Data Flow ``` FRONTEND (User in Eastern Time selects "Dec 8, 2:00 PM") ↓ Convert to UTC: "2024-12-08T19:00:00Z" ↓ Send to API (always UTC) ↓ DATABASE (stores UTC) ↓ API RESPONSE: { "start_time": "2024-12-08T19:00:00Z", // Always UTC "business_timezone": "America/Denver" // IANA timezone (or null for local) } ↓ FRONTEND CONVERTS: - If business_timezone set: UTC → Mountain Time → "Dec 8, 12:00 PM MST" - If business_timezone null: UTC → User local → "Dec 8, 2:00 PM EST" ``` ### Frontend Helper Functions Located in `frontend/src/utils/dateUtils.ts`: ```typescript import { toUTC, fromUTC, formatForDisplay, formatDateForDisplay, getDisplayTimezone, } from '../utils/dateUtils'; // SENDING TO API - Always convert to UTC const apiPayload = { start_time: toUTC(selectedDateTime), // "2024-12-08T19:00:00Z" }; // RECEIVING FROM API - Convert for display const displayTime = formatForDisplay( response.start_time, // UTC from API response.business_timezone // "America/Denver" or null ); // Result: "Dec 8, 2024 12:00 PM" (in business or local timezone) // DATE-ONLY fields (time blocks) const displayDate = formatDateForDisplay( response.start_date, response.business_timezone ); ``` ### API Response Requirements All endpoints returning date/time data MUST include: ```python # In serializers or views { "start_time": "2024-12-08T19:00:00Z", "business_timezone": business.timezone, # "America/Denver" or None } ``` ### Backend Serializer Mixin Use `TimezoneSerializerMixin` from `core/mixins.py` to automatically add the timezone field: ```python from core.mixins import TimezoneSerializerMixin class EventSerializer(TimezoneSerializerMixin, serializers.ModelSerializer): class Meta: model = Event fields = [ 'id', 'start_time', 'end_time', # ... other fields ... 'business_timezone', # Provided by mixin ] read_only_fields = ['business_timezone'] ``` The mixin automatically retrieves the timezone from the tenant context. - Returns the IANA timezone string if set (e.g., "America/Denver") - Returns `null` if not set (frontend uses user's local timezone) ### Common Mistakes to Avoid ```typescript // BAD - Uses browser local time, not UTC date.toISOString().split('T')[0] // BAD - Doesn't respect business timezone setting new Date(utcString).toLocaleString() // GOOD - Use helper functions toUTC(date) // For API requests formatForDisplay(utcString, businessTimezone) // For displaying ``` ## Django App Organization (Domain-Based) Apps are organized into domain packages under `smoothschedule/smoothschedule/`: ### Identity Domain | App | Location | Purpose | |-----|----------|---------| | `core` | `identity/core/` | Tenant, Domain, PermissionGrant, middleware, mixins | | `users` | `identity/users/` | User model, authentication, MFA | ### Scheduling Domain | App | Location | Purpose | |-----|----------|---------| | `schedule` | `scheduling/schedule/` | Resources, Events, Services, Participants | | `contracts` | `scheduling/contracts/` | Contract/e-signature system | | `analytics` | `scheduling/analytics/` | Business analytics and reporting | ### Communication Domain | App | Location | Purpose | |-----|----------|---------| | `notifications` | `communication/notifications/` | Notification system | | `credits` | `communication/credits/` | SMS/calling credits | | `mobile` | `communication/mobile/` | Field employee mobile app | | `messaging` | `communication/messaging/` | Email templates and messaging | ### Commerce Domain | App | Location | Purpose | |-----|----------|---------| | `payments` | `commerce/payments/` | Stripe Connect payments bridge | | `tickets` | `commerce/tickets/` | Support ticket system | ### Platform Domain | App | Location | Purpose | |-----|----------|---------| | `admin` | `platform/admin/` | Platform administration, subscriptions | | `api` | `platform/api/` | Public API v1 for third-party integrations | ## Core Mixins & Base Classes Located in `smoothschedule/smoothschedule/identity/core/mixins.py`. Use these to avoid code duplication. ### Permission Classes ```python from smoothschedule.identity.core.mixins import DenyStaffWritePermission, DenyStaffAllAccessPermission, DenyStaffListPermission class MyViewSet(ModelViewSet): # Block write operations for staff (GET allowed) permission_classes = [IsAuthenticated, DenyStaffWritePermission] # Block ALL operations for staff permission_classes = [IsAuthenticated, DenyStaffAllAccessPermission] # Block list/create/update/delete but allow retrieve permission_classes = [IsAuthenticated, DenyStaffListPermission] ``` #### Per-User Permission Overrides Staff permissions can be overridden on a per-user basis using the `user.permissions` JSONField. Permission keys are auto-derived from the view's basename or model name: | Permission Class | Auto-derived Key | Example | |-----------------|------------------|---------| | `DenyStaffWritePermission` | `can_write_{basename}` | `can_write_resources` | | `DenyStaffAllAccessPermission` | `can_access_{basename}` | `can_access_services` | | `DenyStaffListPermission` | `can_list_{basename}` or `can_access_{basename}` | `can_list_customers` | **Current ViewSet permission keys:** | ViewSet | Permission Class | Override Key | |---------|-----------------|--------------| | `ResourceViewSet` | `DenyStaffAllAccessPermission` | `can_access_resources` | | `ServiceViewSet` | `DenyStaffAllAccessPermission` | `can_access_services` | | `CustomerViewSet` | `DenyStaffListPermission` | `can_list_customers` or `can_access_customers` | | `ScheduledTaskViewSet` | `DenyStaffAllAccessPermission` | `can_access_scheduled-tasks` | **Granting a specific staff member access:** ```bash # Open Django shell docker compose -f docker-compose.local.yml exec django python manage.py shell ``` ```python from smoothschedule.identity.users.models import User # Find the staff member staff = User.objects.get(email='john@example.com') # Grant read access to resources staff.permissions['can_access_resources'] = True staff.save() # Or grant list access to customers (but not full CRUD) staff.permissions['can_list_customers'] = True staff.save() ``` **Custom permission keys (optional):** ```python class ResourceViewSet(ModelViewSet): permission_classes = [IsAuthenticated, DenyStaffAllAccessPermission] # Override the auto-derived key staff_access_permission_key = 'can_manage_equipment' ``` Then grant via: `staff.permissions['can_manage_equipment'] = True` ### QuerySet Mixins ```python from smoothschedule.identity.core.mixins import TenantFilteredQuerySetMixin, UserTenantFilteredMixin # For tenant-scoped models (automatic django-tenants filtering) class ResourceViewSet(TenantFilteredQuerySetMixin, ModelViewSet): queryset = Resource.objects.all() deny_staff_queryset = True # Optional: also filter staff at queryset level def filter_queryset_for_tenant(self, queryset): # Override for custom filtering return queryset.filter(is_active=True) # For User model (shared schema, needs explicit tenant filter) class CustomerViewSet(UserTenantFilteredMixin, ModelViewSet): queryset = User.objects.filter(role=User.Role.CUSTOMER) ``` ### Feature Permission Mixins ```python from smoothschedule.identity.core.mixins import PluginFeatureRequiredMixin, TaskFeatureRequiredMixin # Checks can_use_plugins feature on list/retrieve/create class PluginViewSet(PluginFeatureRequiredMixin, ModelViewSet): pass # Checks both can_use_plugins AND can_use_tasks class ScheduledTaskViewSet(TaskFeatureRequiredMixin, TenantFilteredQuerySetMixin, ModelViewSet): pass ``` ### Base API Views (for non-ViewSet views) ```python from rest_framework.views import APIView from smoothschedule.identity.core.mixins import TenantAPIView, TenantRequiredAPIView # Optional tenant - use self.get_tenant() class MyView(TenantAPIView, APIView): def get(self, request): tenant = self.get_tenant() # May be None return self.success_response({'data': 'value'}) # or: return self.error_response('Something went wrong', status_code=400) # Required tenant - self.tenant always available class MyTenantView(TenantRequiredAPIView, APIView): def get(self, request): # self.tenant is guaranteed to exist (returns 400 if missing) return Response({'name': self.tenant.name}) ``` ### Helper Methods Available | Method | Description | |--------|-------------| | `self.get_tenant()` | Get tenant from request (may be None) | | `self.get_tenant_or_error()` | Returns (tenant, error_response) tuple | | `self.error_response(msg, status_code)` | Standard error response | | `self.success_response(data, status_code)` | Standard success response | | `self.check_feature(key, name)` | Check feature permission, returns error or None | ## Common Tasks ### After modifying Django models: ```bash cd /home/poduck/Desktop/smoothschedule2/smoothschedule docker compose -f docker-compose.local.yml exec django python manage.py makemigrations docker compose -f docker-compose.local.yml exec django python manage.py migrate ``` ### After modifying frontend: Frontend hot-reloads automatically. If issues, restart: ```bash cd /home/poduck/Desktop/smoothschedule2/frontend npm run dev ``` ### Debugging 500 errors: ```bash cd /home/poduck/Desktop/smoothschedule2/smoothschedule docker compose -f docker-compose.local.yml logs django --tail=100 ``` ### Testing API directly: ```bash curl -s "http://lvh.me:8000/api/resources/" | jq ``` ## Git Branch Currently on: `feature/platform-superuser-ui` Main branch: `main` ## Production Deployment ### Quick Deploy ```bash # From your local machine cd /home/poduck/Desktop/smoothschedule2 ./deploy.sh poduck@smoothschedule.com ``` ### Initial Server Setup (one-time) ```bash # Setup server dependencies ssh poduck@smoothschedule.com 'bash -s' < server-setup.sh # Setup DigitalOcean Spaces ssh poduck@smoothschedule.com ./setup-spaces.sh ``` ### Production URLs - **Main site:** `https://smoothschedule.com` - **Platform dashboard:** `https://platform.smoothschedule.com` - **Tenant subdomains:** `https://*.smoothschedule.com` - **Flower (Celery):** `https://smoothschedule.com:5555` ### Production Management ```bash # SSH into server ssh poduck@smoothschedule.com # Navigate to project cd ~/smoothschedule # View logs docker compose -f docker-compose.production.yml logs -f # Run migrations docker compose -f docker-compose.production.yml exec django python manage.py migrate # Create superuser docker compose -f docker-compose.production.yml exec django python manage.py createsuperuser # Restart services docker compose -f docker-compose.production.yml restart # View status docker compose -f docker-compose.production.yml ps ``` ### Environment Variables Production environment configured in: - **Backend:** `smoothschedule/.envs/.production/.django` - **Database:** `smoothschedule/.envs/.production/.postgres` - **Frontend:** `frontend/.env.production` ### DigitalOcean Spaces - **Bucket:** `smoothschedule` - **Region:** `nyc3` - **Endpoint:** `https://nyc3.digitaloceanspaces.com` - **Public URL:** `https://smoothschedule.nyc3.digitaloceanspaces.com` See [DEPLOYMENT.md](DEPLOYMENT.md) for detailed deployment guide.