# 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 ``` ## Database Initialization & Seeding When resetting the database or setting up a fresh environment, follow these steps in order. ### Step 1: Reset Database (if needed) ```bash cd /home/poduck/Desktop/smoothschedule/smoothschedule # Stop all containers and remove volumes (DESTRUCTIVE - removes all data) docker compose -f docker-compose.local.yml down -v # Start services fresh docker compose -f docker-compose.local.yml up -d ``` ### Step 2: Create Activepieces Database The Activepieces service requires its own database: ```bash docker compose -f docker-compose.local.yml exec postgres psql -U FDuVLuQjfpYzGizTmaTavMcaimqpCMRM -d postgres -c "CREATE DATABASE activepieces;" ``` ### Step 3: Initialize Activepieces Platform Create the initial Activepieces admin user (this auto-creates the platform): ```bash curl -s -X POST http://localhost:8090/api/v1/authentication/sign-up \ -H "Content-Type: application/json" \ -d '{"email":"admin@smoothschedule.com","password":"Admin123!","firstName":"Admin","lastName":"User","newsLetter":false,"trackEvents":false}' ``` **IMPORTANT:** Update `.envs/.local/.activepieces` with the returned IDs: - `AP_PLATFORM_ID` - from the `platformId` field in response - `AP_DEFAULT_PROJECT_ID` - from the `projectId` field in response Then restart containers to pick up new IDs: ```bash docker compose -f docker-compose.local.yml restart activepieces django ``` ### Step 4: Run Django Migrations ```bash docker compose -f docker-compose.local.yml exec django python manage.py migrate ``` ### Step 5: Seed Billing Catalog Creates subscription plans (free, starter, growth, pro, enterprise): ```bash docker compose -f docker-compose.local.yml exec django python manage.py billing_seed_catalog ``` ### Step 6: Seed Demo Data Run the reseed_demo command to create the demo tenant with sample data: ```bash docker compose -f docker-compose.local.yml exec django python manage.py reseed_demo ``` This creates: - **Tenant:** "Serenity Salon & Spa" (subdomain: `demo`) - **Pro subscription** with pink theme branding - **Staff:** 6 stylists/therapists with salon/spa themed names - **Services:** 12 salon/spa services (haircuts, coloring, massages, facials, etc.) - **Resources:** 4 treatment rooms - **Customers:** 20 sample customers - **Appointments:** 100 appointments respecting business hours (9am-5pm Mon-Fri) ### Step 7: Create Platform Test Users The DevQuickLogin component expects these users with password `test123`: ```bash docker compose -f docker-compose.local.yml exec django python manage.py shell -c " from smoothschedule.identity.users.models import User # Platform users (public schema) users_data = [ ('superuser@platform.com', 'Super User', 'superuser', True, True), ('manager@platform.com', 'Platform Manager', 'platform_manager', True, False), ('sales@platform.com', 'Sales Rep', 'platform_sales', True, False), ('support@platform.com', 'Support Agent', 'platform_support', True, False), ] for email, name, role, is_staff, is_superuser in users_data: user, created = User.objects.get_or_create( email=email, defaults={ 'username': email, 'name': name, 'role': role, 'is_staff': is_staff, 'is_superuser': is_superuser, 'is_active': True, } ) if created: user.set_password('test123') user.save() print(f'Created: {email}') else: print(f'Exists: {email}') " ``` ### Step 8: Seed Automation Templates Seeds 8 automation templates to the Activepieces template gallery: ```bash docker compose -f docker-compose.local.yml exec django python manage.py seed_automation_templates ``` Templates created: - Appointment Confirmation Email - SMS Appointment Reminder - Staff Notification - New Booking - Cancellation Confirmation Email - Thank You + Google Review Request - Win-back Email Campaign - Google Calendar Sync - Webhook Notification ### Step 9: Provision Activepieces Connections Creates SmoothSchedule connections in Activepieces for all tenants: ```bash docker compose -f docker-compose.local.yml exec django python manage.py provision_ap_connections --force ``` ### Step 10: Provision Default Flows Creates the default email automation flows for each tenant: ```bash docker compose -f docker-compose.local.yml exec django python manage.py shell -c " from smoothschedule.identity.core.models import Tenant from smoothschedule.identity.core.signals import _provision_default_flows_for_tenant for tenant in Tenant.objects.exclude(schema_name='public'): print(f'Provisioning default flows for: {tenant.name}') _provision_default_flows_for_tenant(tenant.id) print('Done!') " ``` Default flows created per tenant: - `appointment_confirmation` - Confirmation email when appointment is booked - `appointment_reminder` - Reminder based on service settings - `thank_you` - Thank you email after final payment - `payment_deposit` - Deposit payment confirmation - `payment_final` - Final payment confirmation ### Quick Reference: All Seed Commands ```bash cd /home/poduck/Desktop/smoothschedule/smoothschedule # 1. Create Activepieces database docker compose -f docker-compose.local.yml exec postgres psql -U FDuVLuQjfpYzGizTmaTavMcaimqpCMRM -d postgres -c "CREATE DATABASE activepieces;" # 2. Run migrations docker compose -f docker-compose.local.yml exec django python manage.py migrate # 3. Seed billing plans docker compose -f docker-compose.local.yml exec django python manage.py billing_seed_catalog # 4. Seed demo tenant with data docker compose -f docker-compose.local.yml exec django python manage.py reseed_demo # 5. Seed automation templates docker compose -f docker-compose.local.yml exec django python manage.py seed_automation_templates # 6. Provision Activepieces connections docker compose -f docker-compose.local.yml exec django python manage.py provision_ap_connections --force # 7. Provision default flows (run in Django shell - see Step 10 above) ``` ### Verifying Setup After seeding, verify everything is working: ```bash # Check all services are running docker compose -f docker-compose.local.yml ps # Check Activepieces health curl -s http://localhost:8090/api/v1/health # Test login curl -s http://api.lvh.me:8000/auth/login/ -X POST \ -H "Content-Type: application/json" \ -d '{"email":"owner@demo.com","password":"test123"}' # Check flows exist in Activepieces docker compose -f docker-compose.local.yml exec postgres psql -U FDuVLuQjfpYzGizTmaTavMcaimqpCMRM -d activepieces -c "SELECT id, status FROM flow;" ``` Access the application at: - **Demo tenant:** http://demo.lvh.me:5173 - **Platform:** http://platform.lvh.me:5173 ## 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.