# SmoothSchedule Backend Development Guide

## Docker-Based Development

**IMPORTANT:** This project runs in Docker containers. Do NOT try to run Django commands directly on the host machine - they will fail due to missing environment variables and database connection.

### Running Django Commands

Always use Docker Compose to execute commands:

```bash
# Navigate to the smoothschedule directory first
cd /home/poduck/Desktop/smoothschedule2/smoothschedule

# Run migrations
docker compose -f docker-compose.local.yml exec django python manage.py migrate

# Run migrations for a specific app
docker compose -f docker-compose.local.yml exec django python manage.py migrate schedule

# Make migrations
docker compose -f docker-compose.local.yml exec django python manage.py makemigrations

# Create superuser
docker compose -f docker-compose.local.yml exec django python manage.py createsuperuser

# Run management commands
docker compose -f docker-compose.local.yml exec django python manage.py <command>

# Access Django shell
docker compose -f docker-compose.local.yml exec django python manage.py shell

# Run tests
docker compose -f docker-compose.local.yml exec django pytest
```

### Multi-Tenant Migrations

This is a multi-tenant app using django-tenants. To run migrations on a specific tenant schema:

```bash
# Run on a specific tenant (e.g., 'demo')
docker compose -f docker-compose.local.yml exec django python manage.py tenant_command migrate --schema=demo

# Run on public schema
docker compose -f docker-compose.local.yml exec django python manage.py migrate_schemas --shared
```

### Docker Services

```bash
# Start all services
docker compose -f docker-compose.local.yml up -d

# View logs
docker compose -f docker-compose.local.yml logs -f django

# Restart Django after code changes (usually auto-reloads)
docker compose -f docker-compose.local.yml restart django

# Rebuild after dependency changes
docker compose -f docker-compose.local.yml up -d --build
```

## Project Structure

### Critical Configuration Files

```
smoothschedule/
├── docker-compose.local.yml          # Local development Docker config
├── docker-compose.production.yml     # Production Docker config
├── .envs/
│   ├── .local/
│   │   ├── .django                   # Django env vars (SECRET_KEY, DEBUG, etc.)
│   │   └── .postgres                 # Database credentials
│   └── .production/
│       ├── .django
│       └── .postgres
├── config/
│   ├── settings/
│   │   ├── base.py                   # Base settings (shared)
│   │   ├── local.py                  # Local dev settings (imports multitenancy.py)
│   │   ├── production.py             # Production settings
│   │   ├── multitenancy.py           # Multi-tenant configuration
│   │   └── test.py                   # Test settings
│   └── urls.py                       # Main URL configuration
├── compose/
│   ├── local/django/
│   │   ├── Dockerfile                # Local Django container
│   │   └── start                     # Startup script
│   └── production/
│       ├── django/
│       ├── postgres/
│       └── traefik/
```

### Django Apps (Domain-Based Organization)

```
smoothschedule/smoothschedule/
├── identity/                         # Identity Domain
│   ├── core/                         # Tenant, Domain, middleware, mixins
│   │   ├── models.py                 # Tenant, Domain, PermissionGrant
│   │   ├── middleware.py             # TenantHeader, Sandbox, Masquerade
│   │   └── mixins.py                 # Base classes for views/viewsets
│   └── users/                        # User management, authentication
│       ├── models.py                 # User model with roles
│       ├── api_views.py              # Auth endpoints
│       └── mfa_api_views.py          # MFA endpoints
├── scheduling/                       # Scheduling Domain
│   ├── schedule/                     # Core scheduling functionality
│   │   ├── models.py                 # Resource, Event, Service, Participant
│   │   ├── serializers.py            # DRF serializers
│   │   ├── views.py                  # ViewSets for API
│   │   └── services.py               # AvailabilityService
│   ├── contracts/                    # E-signature system
│   └── analytics/                    # Business analytics
├── communication/                    # Communication Domain
│   ├── notifications/                # Notification system
│   ├── credits/                      # SMS/calling credits
│   ├── mobile/                       # Field employee mobile app
│   └── messaging/                    # Email templates
├── commerce/                         # Commerce Domain
│   ├── payments/                     # Stripe Connect integration
│   └── tickets/                      # Support tickets
└── platform/                         # Platform Domain
    ├── admin/                        # Platform administration
    └── api/                          # Public API v1
```

### API Endpoints

Base URL: `http://lvh.me:8000/api/`

- `/api/resources/` - Resource CRUD
- `/api/events/` - Event/Appointment CRUD
- `/api/services/` - Service CRUD
- `/api/customers/` - Customer listing
- `/api/auth/login/` - Authentication
- `/api/auth/logout/` - Logout
- `/api/users/me/` - Current user info
- `/api/business/` - Business settings

## Local Development URLs

- **Backend API:** `http://lvh.me:8000`
- **Frontend:** `http://demo.lvh.me:5173` (business subdomain)
- **Platform Frontend:** `http://platform.lvh.me:5173`

Note: `lvh.me` resolves to `127.0.0.1` and allows subdomain-based multi-tenancy with cookies.

## Database

- **Type:** PostgreSQL with django-tenants
- **Public Schema:** Shared tables (tenants, domains, platform users)
- **Tenant Schemas:** Per-business data (resources, events, customers)

## Common Issues

### 500 Error with No CORS Headers
When Django crashes (500 error), CORS headers aren't sent. Check Django logs:
```bash
docker compose -f docker-compose.local.yml logs django --tail=100
```

### Missing Column/Table Errors
Run migrations:
```bash
docker compose -f docker-compose.local.yml exec django python manage.py migrate
```

### ModuleNotFoundError / ImportError
You're trying to run Python directly instead of through Docker. Use `docker compose exec`.

## Key Models

### Resource (scheduling/schedule/models.py)
- `name`, `type` (STAFF/ROOM/EQUIPMENT)
- `max_concurrent_events` - concurrency limit (1=exclusive, >1=multilane, 0=unlimited)
- `saved_lane_count` - remembers lane count when multilane disabled
- `buffer_duration` - time between events

### Event (scheduling/schedule/models.py)
- `title`, `start_time`, `end_time`, `status`
- Links to resources/customers via `Participant` model

### User (identity/users/models.py)
- Roles: `superuser`, `platform_manager`, `platform_support`, `owner`, `manager`, `staff`, `resource`, `customer`
- `business_subdomain` - which tenant they belong to

### Tenant (identity/core/models.py)
- `name`, `subdomain`, `schema_name`
- Multi-tenancy via django-tenants

## Testing Guidelines

### Testing Philosophy

Follow the **Testing Pyramid**:
```
        /\
       /E2E\        <- Few (real browser, full stack) - AVOID
      /------\
     /Integr- \     <- Some (real DB, verify ORM/queries)
    /---ation--\
   /   Unit     \   <- Many (mocked, fast, isolated) - PREFER
  /--------------\
```

**Why:** Django-tenants requires full PostgreSQL migrations per test, making DB-based tests extremely slow (~30-60 seconds each). Unit tests with mocks run in milliseconds.

### Unit Tests (Preferred)

Write fast, isolated unit tests using mocks. **Do NOT use `@pytest.mark.django_db`** for unit tests.

```python
# GOOD: Fast unit test with mocks
from unittest.mock import Mock, patch

def test_event_service_creates_event():
    # Arrange - mock dependencies
    mock_repo = Mock()
    mock_repo.save.return_value = Mock(id=1, title="Test Event")

    mock_tenant = Mock()
    mock_tenant.can_use_feature.return_value = True

    service = EventService(repository=mock_repo, tenant=mock_tenant)

    # Act
    result = service.create_event(title="Test Event", start_time=datetime.now())

    # Assert
    mock_repo.save.assert_called_once()
    assert result.title == "Test Event"


def test_event_service_denies_without_permission():
    mock_tenant = Mock()
    mock_tenant.can_use_feature.return_value = False

    service = EventService(repository=Mock(), tenant=mock_tenant)

    with pytest.raises(PermissionDenied):
        service.create_event(title="Test")
```

```python
# BAD: Slow test hitting real database
@pytest.mark.django_db
def test_event_creation(tenant, user):
    # This takes 30+ seconds due to tenant schema setup!
    event = Event.objects.create(tenant=tenant, title="Test")
    assert event.id is not None
```

### Testing Serializers

```python
def test_event_serializer_validates_end_after_start():
    data = {
        'title': 'Test',
        'start_time': '2024-01-01T10:00:00Z',
        'end_time': '2024-01-01T09:00:00Z',  # Before start!
    }
    serializer = EventSerializer(data=data)

    assert not serializer.is_valid()
    assert 'end_time' in serializer.errors
```

### Testing Views/ViewSets

Use `APIRequestFactory` with mocked authentication:

```python
from rest_framework.test import APIRequestFactory
from unittest.mock import Mock, patch

def test_resource_list_returns_filtered_resources():
    factory = APIRequestFactory()
    request = factory.get('/api/resources/')

    # Mock the user and tenant
    request.user = Mock(is_authenticated=True, role='manager')
    request.tenant = Mock(id=1)

    # Mock the queryset
    with patch.object(ResourceViewSet, 'get_queryset') as mock_qs:
        mock_qs.return_value = [Mock(id=1, name='Room A')]

        view = ResourceViewSet.as_view({'get': 'list'})
        response = view(request)

    assert response.status_code == 200
```

### Integration Tests (Use Sparingly)

Only use `@pytest.mark.django_db` when you MUST verify:
- Complex ORM queries with joins/aggregations
- Database constraints and triggers
- Migration correctness

```python
# Integration test - use only when necessary
@pytest.mark.django_db
class TestEventQueryIntegration:
    """Verify complex queries work correctly with real DB."""

    def test_overlapping_events_query(self, tenant_with_events):
        # This tests the actual SQL query behavior
        overlapping = Event.objects.filter_overlapping(
            start=datetime(2024, 1, 1, 10),
            end=datetime(2024, 1, 1, 11)
        )
        assert overlapping.count() == 2
```

### Writing Testable Code

Design code for testability using dependency injection:

```python
# BAD: Hard to test - direct imports and global state
class EventService:
    def create_event(self, data):
        tenant = get_current_tenant()  # Global state!
        event = Event.objects.create(**data)  # Direct ORM call
        send_notification(event)  # Direct import
        return event

# GOOD: Easy to test - dependencies injected
class EventService:
    def __init__(self, repository, tenant, notifier):
        self.repository = repository
        self.tenant = tenant
        self.notifier = notifier

    def create_event(self, data):
        if not self.tenant.can_use_feature('events'):
            raise PermissionDenied()
        event = self.repository.save(Event(**data))
        self.notifier.send(event)
        return event
```

### Repository Pattern

Abstract database access for easier mocking:

```python
# In repositories.py
class EventRepository:
    def save(self, event):
        event.save()
        return event

    def get_by_id(self, event_id):
        return Event.objects.get(id=event_id)

    def filter_by_date_range(self, start, end):
        return Event.objects.filter(start_time__gte=start, end_time__lte=end)

# In tests - mock the repository
def test_something():
    mock_repo = Mock(spec=EventRepository)
    mock_repo.get_by_id.return_value = Mock(id=1, title="Test")
    # ... test with mock_repo
```

### Test File Structure

Each app should have:
```
app/
├── tests/
│   ├── __init__.py
│   ├── test_services.py      # Unit tests for business logic
│   ├── test_serializers.py   # Serializer validation tests
│   ├── test_views.py         # View/ViewSet tests (mocked)
│   └── test_integration.py   # DB tests (sparingly)
```

### Running Tests

```bash
# Run all tests
docker compose -f docker-compose.local.yml exec django pytest

# Run specific test file
docker compose -f docker-compose.local.yml exec django pytest smoothschedule/scheduling/schedule/tests/test_services.py

# Run with coverage
docker compose -f docker-compose.local.yml exec django coverage run -m pytest
docker compose -f docker-compose.local.yml exec django coverage report

# Run only unit tests (fast)
docker compose -f docker-compose.local.yml exec django pytest -m "not django_db"

# Run only integration tests
docker compose -f docker-compose.local.yml exec django pytest -m django_db
```

### Key Testing Rules

1. **Default to mocks** - Only hit DB when testing query logic
2. **One assertion focus** - Each test verifies one behavior
3. **Descriptive names** - `test_create_event_raises_when_tenant_lacks_permission`
4. **Arrange-Act-Assert** - Clear test structure
5. **No test interdependence** - Tests must run in any order
6. **Fast feedback** - If a test takes >1 second, it probably shouldn't hit the DB
