refactor: Reorganize Django apps into domain-based structure

Restructured 13 Django apps from flat/mixed organization into 5 logical
domain packages following cookiecutter-django conventions:

- identity/: core (tenant/domain models, middleware, mixins), users
- scheduling/: schedule, contracts, analytics
- communication/: notifications, credits, mobile, messaging
- commerce/: payments, tickets
- platform/: admin, api

Key changes:
- Moved all apps to smoothschedule/smoothschedule/{domain}/{app}/
- Updated all import paths across the codebase
- Updated settings (base.py, multitenancy.py, test.py)
- Updated URL configuration in config/urls.py
- Updated middleware and permission paths
- Preserved app_label in AppConfig for migration compatibility
- Updated CLAUDE.md documentation with new structure

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-07 18:24:50 -05:00
parent 410b46a896
commit 156cc2676d
324 changed files with 865 additions and 360 deletions

View File

@@ -62,25 +62,51 @@ docker compose -f docker-compose.local.yml exec django python manage.py <command
| `frontend/src/types.ts` | TypeScript interfaces |
| `frontend/src/i18n/locales/en.json` | Translations |
## Key Django Apps
## Django App Organization (Domain-Based)
Apps are organized into domain packages under `smoothschedule/smoothschedule/`:
### Identity Domain
| App | Location | Purpose |
|-----|----------|---------|
| `schedule` | `smoothschedule/smoothschedule/schedule/` | Resources, Events, Services |
| `users` | `smoothschedule/smoothschedule/users/` | Authentication, User model |
| `tenants` | `smoothschedule/smoothschedule/tenants/` | Multi-tenancy (Business model) |
| `core` | `smoothschedule/core/` | Shared mixins, permissions, middleware |
| `payments` | `smoothschedule/payments/` | Stripe integration, subscriptions |
| `platform_admin` | `smoothschedule/platform_admin/` | Platform administration |
| `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/core/mixins.py`. Use these to avoid code duplication.
Located in `smoothschedule/smoothschedule/identity/core/mixins.py`. Use these to avoid code duplication.
### Permission Classes
```python
from core.mixins import DenyStaffWritePermission, DenyStaffAllAccessPermission, DenyStaffListPermission
from smoothschedule.identity.core.mixins import DenyStaffWritePermission, DenyStaffAllAccessPermission, DenyStaffListPermission
class MyViewSet(ModelViewSet):
# Block write operations for staff (GET allowed)
@@ -120,7 +146,7 @@ docker compose -f docker-compose.local.yml exec django python manage.py shell
```
```python
from smoothschedule.users.models import User
from smoothschedule.identity.users.models import User
# Find the staff member
staff = User.objects.get(email='john@example.com')
@@ -147,7 +173,7 @@ Then grant via: `staff.permissions['can_manage_equipment'] = True`
### QuerySet Mixins
```python
from core.mixins import TenantFilteredQuerySetMixin, UserTenantFilteredMixin
from smoothschedule.identity.core.mixins import TenantFilteredQuerySetMixin, UserTenantFilteredMixin
# For tenant-scoped models (automatic django-tenants filtering)
class ResourceViewSet(TenantFilteredQuerySetMixin, ModelViewSet):
@@ -166,7 +192,7 @@ class CustomerViewSet(UserTenantFilteredMixin, ModelViewSet):
### Feature Permission Mixins
```python
from core.mixins import PluginFeatureRequiredMixin, TaskFeatureRequiredMixin
from smoothschedule.identity.core.mixins import PluginFeatureRequiredMixin, TaskFeatureRequiredMixin
# Checks can_use_plugins feature on list/retrieve/create
class PluginViewSet(PluginFeatureRequiredMixin, ModelViewSet):
@@ -181,7 +207,7 @@ class ScheduledTaskViewSet(TaskFeatureRequiredMixin, TenantFilteredQuerySetMixin
```python
from rest_framework.views import APIView
from core.mixins import TenantAPIView, TenantRequiredAPIView
from smoothschedule.identity.core.mixins import TenantAPIView, TenantRequiredAPIView
# Optional tenant - use self.get_tenant()
class MyView(TenantAPIView, APIView):

383
PLAN_APP_REORGANIZATION.md Normal file
View File

@@ -0,0 +1,383 @@
# Django App Reorganization Plan - Option C (Domain-Based)
## Overview
Reorganize Django apps from their current scattered locations into a clean domain-based structure within `smoothschedule/smoothschedule/`.
**Branch:** `refactor/organize-django-apps`
**Risk Level:** Medium-High (migration history must be preserved)
**Estimated Parallel Agents:** 6-8
---
## Current State Analysis
### Current App Locations (Inconsistent)
| App | Current Location | Registered As |
|-----|-----------------|---------------|
| core | `smoothschedule/core/` | `"core"` |
| schedule | `smoothschedule/schedule/` | `"schedule"` |
| payments | `smoothschedule/payments/` | `"payments"` |
| platform_admin | `smoothschedule/platform_admin/` | `"platform_admin.apps.PlatformAdminConfig"` |
| analytics | `smoothschedule/analytics/` | `"analytics"` |
| notifications | `smoothschedule/notifications/` | `"notifications"` |
| tickets | `smoothschedule/tickets/` | `"tickets"` |
| contracts | `smoothschedule/contracts/` | **NOT REGISTERED** |
| communication | `smoothschedule/communication/` | **NOT REGISTERED** |
| users | `smoothschedule/smoothschedule/users/` | `"smoothschedule.users"` |
| comms_credits | `smoothschedule/smoothschedule/comms_credits/` | `"smoothschedule.comms_credits"` |
| field_mobile | `smoothschedule/smoothschedule/field_mobile/` | `"smoothschedule.field_mobile"` |
| public_api | `smoothschedule/smoothschedule/public_api/` | `"smoothschedule.public_api"` |
### Migration Counts by App
| App | Migrations | Complexity |
|-----|------------|------------|
| core | 22 | High (Tenant model) |
| schedule | 30 | High (main business logic) |
| payments | 1 | Low |
| platform_admin | 12 | Medium |
| users | 10 | Medium |
| tickets | 13 | Medium |
| contracts | 1 | Low |
| notifications | 1 | Low |
| comms_credits | 2 | Low |
| field_mobile | 1 | Low |
| public_api | 3 | Low |
| analytics | 0 | None |
| communication | 1 | Low |
---
## Target Structure (Option C - Domain-Based)
```
smoothschedule/smoothschedule/
├── __init__.py
├── identity/ # User & Tenant Management
│ ├── __init__.py
│ ├── core/ # Multi-tenancy, permissions, OAuth
│ │ └── (moved from smoothschedule/core/)
│ └── users/ # User model, auth, invitations
│ └── (keep at current location, just move parent)
├── scheduling/ # Core Business Logic
│ ├── __init__.py
│ ├── schedule/ # Resources, Events, Services, Plugins
│ │ └── (moved from smoothschedule/schedule/)
│ ├── contracts/ # E-signatures, legal documents
│ │ └── (moved from smoothschedule/contracts/)
│ └── analytics/ # Reporting, dashboards
│ └── (moved from smoothschedule/analytics/)
├── communication/ # Messaging & Notifications
│ ├── __init__.py
│ ├── notifications/ # In-app notifications
│ │ └── (moved from smoothschedule/notifications/)
│ ├── credits/ # SMS/voice credits (renamed from comms_credits)
│ │ └── (moved from smoothschedule/smoothschedule/comms_credits/)
│ ├── mobile/ # Field employee app (renamed from field_mobile)
│ │ └── (moved from smoothschedule/smoothschedule/field_mobile/)
│ └── messaging/ # Twilio conversations (renamed from communication)
│ └── (moved from smoothschedule/communication/)
├── commerce/ # Payments & Support
│ ├── __init__.py
│ ├── payments/ # Stripe Connect, transactions
│ │ └── (moved from smoothschedule/payments/)
│ └── tickets/ # Support tickets, email integration
│ └── (moved from smoothschedule/tickets/)
└── platform/ # Platform Administration
├── __init__.py
├── admin/ # Platform settings, subscriptions (renamed)
│ └── (moved from smoothschedule/platform_admin/)
└── api/ # Public API v1 (renamed from public_api)
└── (moved from smoothschedule/smoothschedule/public_api/)
```
---
## Critical Constraints
### 1. Migration History Preservation
Django migrations contain the app label in their `dependencies` and `app_label` references. We MUST:
- **Keep `app_label` unchanged** in each app's `Meta` class
- Update `AppConfig.name` to the new dotted path
- Django will use the `app_label` (not the path) for migration tracking
### 2. Foreign Key String References
Models use string references like `'users.User'` and `'core.Tenant'`. These reference `app_label`, not the module path, so they remain valid.
### 3. Import Path Updates
All imports across the codebase must be updated:
- `from core.models import Tenant``from smoothschedule.identity.core.models import Tenant`
- `from schedule.models import Event``from smoothschedule.scheduling.schedule.models import Event`
### 4. URL Configuration
`config/urls.py` imports views directly - all import paths must be updated.
### 5. Settings Files
- `config/settings/base.py` - `LOCAL_APPS`
- `config/settings/multitenancy.py` - `SHARED_APPS`, `TENANT_APPS`
---
## Implementation Phases
### Phase 1: Preparation (Serial)
**Agent 1: Setup & Verification**
1. Create all domain package directories with `__init__.py` files
2. Verify Docker is running and database is accessible
3. Run existing tests to establish baseline
4. Create backup of current migration state
```bash
# Create domain packages
mkdir -p smoothschedule/smoothschedule/identity
mkdir -p smoothschedule/smoothschedule/scheduling
mkdir -p smoothschedule/smoothschedule/communication
mkdir -p smoothschedule/smoothschedule/commerce
mkdir -p smoothschedule/smoothschedule/platform
# Create __init__.py files
touch smoothschedule/smoothschedule/identity/__init__.py
touch smoothschedule/smoothschedule/scheduling/__init__.py
touch smoothschedule/smoothschedule/communication/__init__.py
touch smoothschedule/smoothschedule/commerce/__init__.py
touch smoothschedule/smoothschedule/platform/__init__.py
```
---
### Phase 2: Move Apps (Parallel - 5 Agents)
Each agent handles one domain. For each app move:
1. **Move directory** to new location
2. **Update `apps.py`** - change `name` to new dotted path, keep `label` same
3. **Update internal imports** within the app
4. **Add explicit `app_label`** to all model Meta classes (if not present)
#### Agent 2: Identity Domain
Move and update:
- `smoothschedule/core/``smoothschedule/smoothschedule/identity/core/`
- `smoothschedule/smoothschedule/users/``smoothschedule/smoothschedule/identity/users/`
**apps.py changes:**
```python
# identity/core/apps.py
class CoreConfig(AppConfig):
name = "smoothschedule.identity.core" # NEW
label = "core" # KEEP SAME
verbose_name = "Core"
# identity/users/apps.py
class UsersConfig(AppConfig):
name = "smoothschedule.identity.users" # NEW
label = "users" # KEEP SAME
```
#### Agent 3: Scheduling Domain
Move and update:
- `smoothschedule/schedule/``smoothschedule/smoothschedule/scheduling/schedule/`
- `smoothschedule/contracts/``smoothschedule/smoothschedule/scheduling/contracts/`
- `smoothschedule/analytics/``smoothschedule/smoothschedule/scheduling/analytics/`
#### Agent 4: Communication Domain
Move and update:
- `smoothschedule/notifications/``smoothschedule/smoothschedule/communication/notifications/`
- `smoothschedule/smoothschedule/comms_credits/``smoothschedule/smoothschedule/communication/credits/`
- `smoothschedule/smoothschedule/field_mobile/``smoothschedule/smoothschedule/communication/mobile/`
- `smoothschedule/communication/``smoothschedule/smoothschedule/communication/messaging/`
**Note:** Rename apps for clarity:
- `comms_credits` label stays same, path changes
- `field_mobile` label stays same, path changes
- `communication` label stays same, path changes
#### Agent 5: Commerce Domain
Move and update:
- `smoothschedule/payments/``smoothschedule/smoothschedule/commerce/payments/`
- `smoothschedule/tickets/``smoothschedule/smoothschedule/commerce/tickets/`
#### Agent 6: Platform Domain
Move and update:
- `smoothschedule/platform_admin/``smoothschedule/smoothschedule/platform/admin/`
- `smoothschedule/smoothschedule/public_api/``smoothschedule/smoothschedule/platform/api/`
---
### Phase 3: Update Settings (Serial)
**Agent 7: Settings Configuration**
Update `config/settings/base.py`:
```python
LOCAL_APPS = [
# Identity
"smoothschedule.identity.users",
"smoothschedule.identity.core",
# Scheduling
"smoothschedule.scheduling.schedule",
"smoothschedule.scheduling.contracts",
"smoothschedule.scheduling.analytics",
# Communication
"smoothschedule.communication.notifications",
"smoothschedule.communication.credits",
"smoothschedule.communication.mobile",
"smoothschedule.communication.messaging",
# Commerce
"smoothschedule.commerce.payments",
"smoothschedule.commerce.tickets",
# Platform
"smoothschedule.platform.admin",
"smoothschedule.platform.api",
]
```
Update `config/settings/multitenancy.py`:
```python
SHARED_APPS = [
'django_tenants',
'smoothschedule.identity.core',
'smoothschedule.platform.admin',
# ... rest of shared apps with new paths
]
TENANT_APPS = [
'django.contrib.contenttypes',
'smoothschedule.scheduling.schedule',
'smoothschedule.commerce.payments',
'smoothschedule.scheduling.contracts',
]
```
---
### Phase 4: Update All Import Paths (Parallel - Multiple Agents)
**This is the largest task.** Each agent handles specific import patterns:
#### Agent 8: Core Imports
Find and replace across entire codebase:
- `from core.models import``from smoothschedule.identity.core.models import`
- `from core.``from smoothschedule.identity.core.`
- `import core``import smoothschedule.identity.core as core`
#### Agent 9: Schedule Imports
- `from schedule.models import``from smoothschedule.scheduling.schedule.models import`
- `from schedule.``from smoothschedule.scheduling.schedule.`
#### Agent 10: Users/Auth Imports
- `from smoothschedule.users.``from smoothschedule.identity.users.`
- `from users.``from smoothschedule.identity.users.`
#### Agent 11: Other App Imports
Handle remaining apps:
- payments, tickets, notifications, contracts, analytics
- platform_admin, public_api, comms_credits, field_mobile, communication
---
### Phase 5: URL Configuration Updates (Serial)
**Agent 12: URL Updates**
Update `config/urls.py` with new import paths:
```python
# Old
from schedule.views import ResourceViewSet, EventViewSet
from core.api_views import business_current
# New
from smoothschedule.scheduling.schedule.views import ResourceViewSet, EventViewSet
from smoothschedule.identity.core.api_views import business_current
```
---
### Phase 6: Cleanup & Verification (Serial)
**Agent 13: Cleanup**
1. Remove old empty directories at top level
2. Remove deprecated `smoothschedule/smoothschedule/schedule/` directory
3. Update `CLAUDE.md` documentation
4. Update any remaining references
**Agent 14: Verification**
1. Run `docker compose exec django python manage.py check`
2. Run `docker compose exec django python manage.py makemigrations --check`
3. Run `docker compose exec django python manage.py migrate --check`
4. Run test suite
5. Manual smoke test of key endpoints
---
## App Label Mapping Reference
| Old Import Path | New Import Path | app_label (unchanged) |
|----------------|-----------------|----------------------|
| `core` | `smoothschedule.identity.core` | `core` |
| `smoothschedule.users` | `smoothschedule.identity.users` | `users` |
| `schedule` | `smoothschedule.scheduling.schedule` | `schedule` |
| `contracts` | `smoothschedule.scheduling.contracts` | `contracts` |
| `analytics` | `smoothschedule.scheduling.analytics` | `analytics` |
| `notifications` | `smoothschedule.communication.notifications` | `notifications` |
| `smoothschedule.comms_credits` | `smoothschedule.communication.credits` | `comms_credits` |
| `smoothschedule.field_mobile` | `smoothschedule.communication.mobile` | `field_mobile` |
| `communication` | `smoothschedule.communication.messaging` | `communication` |
| `payments` | `smoothschedule.commerce.payments` | `payments` |
| `tickets` | `smoothschedule.commerce.tickets` | `tickets` |
| `platform_admin` | `smoothschedule.platform.admin` | `platform_admin` |
| `smoothschedule.public_api` | `smoothschedule.platform.api` | `public_api` |
---
## Rollback Plan
If issues are encountered:
1. **Git Reset:** `git checkout main` and delete branch
2. **Database:** No migration changes, database remains intact
3. **Docker:** Rebuild containers if needed
---
## Success Criteria
- [ ] All apps moved to domain-based structure
- [ ] `python manage.py check` passes
- [ ] `python manage.py makemigrations --check` shows no changes
- [ ] All existing tests pass
- [ ] Frontend can communicate with API
- [ ] Mobile app can communicate with API
- [ ] CLAUDE.md updated with new structure
---
## Execution Order
```
Phase 1 (Serial): Agent 1 - Setup
Phase 2 (Parallel): Agents 2-6 - Move apps by domain
Phase 3 (Serial): Agent 7 - Update settings
Phase 4 (Parallel): Agents 8-11 - Update imports
Phase 5 (Serial): Agent 12 - URL updates
Phase 6 (Serial): Agents 13-14 - Cleanup & verify
```
**Total Agents:** 14 (8 can run in parallel at peak)

View File

@@ -289,7 +289,7 @@ docker compose -f docker-compose.local.yml exec django python manage.py migrate
from django.test import TestCase, RequestFactory
from rest_framework.test import APITestCase
from core.models import Tenant
from smoothschedule.users.models import User
from smoothschedule.identity.users.models import User
class CalendarSyncTests(APITestCase):
def setUp(self):

View File

@@ -95,23 +95,38 @@ smoothschedule/
│ └── traefik/
```
### Django Apps
### Django Apps (Domain-Based Organization)
```
smoothschedule/smoothschedule/
├── users/ # User management, authentication
│ ├── models.py # User model with roles
│ ├── api_views.py # Auth endpoints, user API
└── migrations/
├── schedule/ # Core scheduling functionality
── models.py # Resource, Event, Service, Participant
├── serializers.py # DRF serializers
├── views.py # ViewSets for API
├── services.py # AvailabilityService
│ └── migrations/
├── tenants/ # Multi-tenancy (Business/Tenant models)
│ ├── models.py # Tenant, Domain models
└── migrations/
├── 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
@@ -160,16 +175,20 @@ You're trying to run Python directly instead of through Docker. Use `docker comp
## Key Models
### Resource (schedule/models.py)
### 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 (schedule/models.py)
### Event (scheduling/schedule/models.py)
- `title`, `start_time`, `end_time`, `status`
- Links to resources/customers via `Participant` model
### User (users/models.py)
### 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

View File

@@ -159,7 +159,7 @@ docker-compose -f docker-compose.local.yml run --rm django python manage.py migr
In Django shell or admin, create users with different roles:
```python
from smoothschedule.users.models import User
from smoothschedule.identity.users.models import User
from core.models import Tenant
# Get the tenant

View File

@@ -2,7 +2,7 @@ from django.conf import settings
from rest_framework.routers import DefaultRouter
from rest_framework.routers import SimpleRouter
from smoothschedule.users.api.views import UserViewSet
from smoothschedule.identity.users.api.views import UserViewSet
router = DefaultRouter() if settings.DEBUG else SimpleRouter()

View File

@@ -10,9 +10,9 @@ django_asgi_app = get_asgi_application()
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from tickets import routing as tickets_routing
from schedule import routing as schedule_routing
from tickets.middleware import TokenAuthMiddleware
from smoothschedule.commerce.tickets import routing as tickets_routing
from smoothschedule.scheduling.schedule import routing as schedule_routing
from smoothschedule.commerce.tickets.middleware import TokenAuthMiddleware
application = ProtocolTypeRouter(

View File

@@ -97,17 +97,28 @@ THIRD_PARTY_APPS = [
]
LOCAL_APPS = [
"smoothschedule.users",
"core",
"schedule",
"analytics",
"payments",
"platform_admin.apps.PlatformAdminConfig",
"notifications", # New: Generic notification app
"tickets", # New: Support tickets app
"smoothschedule.comms_credits", # Communication credits and SMS/calling
"smoothschedule.field_mobile", # Field employee mobile app
# Your stuff: custom apps go here
# Identity Domain
"smoothschedule.identity.users",
"smoothschedule.identity.core",
# Scheduling Domain
"smoothschedule.scheduling.schedule",
"smoothschedule.scheduling.contracts",
"smoothschedule.scheduling.analytics",
# Communication Domain
"smoothschedule.communication.notifications",
"smoothschedule.communication.credits", # SMS/calling credits (was comms_credits)
"smoothschedule.communication.mobile", # Field employee app (was field_mobile)
"smoothschedule.communication.messaging", # Twilio conversations (was communication)
# Commerce Domain
"smoothschedule.commerce.payments",
"smoothschedule.commerce.tickets",
# Platform Domain
"smoothschedule.platform.admin", # Platform settings (was platform_admin)
"smoothschedule.platform.api", # Public API v1 (was public_api)
]
# https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
@@ -183,7 +194,7 @@ TEMPLATES = [
"django.template.context_processors.media",
"django.template.context_processors.static",
"django.template.context_processors.tz",
"smoothschedule.users.context_processors.allauth_settings",
"smoothschedule.identity.users.context_processors.allauth_settings",
],
},
},
@@ -318,13 +329,13 @@ ACCOUNT_SIGNUP_FIELDS = ["email*", "username*", "password1*", "password2*"]
# https://docs.allauth.org/en/latest/account/configuration.html
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
# https://docs.allauth.org/en/latest/account/configuration.html
ACCOUNT_ADAPTER = "smoothschedule.users.adapters.AccountAdapter"
ACCOUNT_ADAPTER = "smoothschedule.identity.users.adapters.AccountAdapter"
# https://docs.allauth.org/en/latest/account/forms.html
ACCOUNT_FORMS = {"signup": "smoothschedule.users.forms.UserSignupForm"}
ACCOUNT_FORMS = {"signup": "smoothschedule.identity.users.forms.UserSignupForm"}
# https://docs.allauth.org/en/latest/socialaccount/configuration.html
SOCIALACCOUNT_ADAPTER = "smoothschedule.users.adapters.SocialAccountAdapter"
SOCIALACCOUNT_ADAPTER = "smoothschedule.identity.users.adapters.SocialAccountAdapter"
# https://docs.allauth.org/en/latest/socialaccount/configuration.html
SOCIALACCOUNT_FORMS = {"signup": "smoothschedule.users.forms.UserSocialSignupForm"}
SOCIALACCOUNT_FORMS = {"signup": "smoothschedule.identity.users.forms.UserSocialSignupForm"}
# django-rest-framework
# -------------------------------------------------------------------------------

View File

@@ -13,10 +13,16 @@ from .base import INSTALLED_APPS, MIDDLEWARE, DATABASES, LOGGING, env
# Shared apps - Available to all tenants (stored in 'public' schema)
SHARED_APPS = [
'django_tenants', # Must be first
'core', # Core models (Tenant, Domain, PermissionGrant)
'platform_admin.apps.PlatformAdminConfig', # Platform management (TenantInvitation, etc.)
# Django built-ins (must be in shared
# Identity Domain (shared)
'smoothschedule.identity.core', # Core models (Tenant, Domain, PermissionGrant)
'smoothschedule.identity.users', # Users app (shared across tenants)
# Platform Domain (shared)
'smoothschedule.platform.admin', # Platform management (TenantInvitation, etc.)
'smoothschedule.platform.api', # Public API v1 for third-party integrations
# Django built-ins (must be in shared)
'django.contrib.contenttypes',
'django.contrib.auth',
'django.contrib.sessions',
@@ -25,15 +31,12 @@ SHARED_APPS = [
'django.contrib.staticfiles',
'django.contrib.admin',
# Users app (shared across tenants)
'smoothschedule.users',
# Third-party apps that should be shared
'rest_framework',
'rest_framework.authtoken',
'corsheaders',
'drf_spectacular',
'channels', # WebSockets
'channels', # WebSockets
'allauth',
'allauth.account',
'allauth.mfa',
@@ -45,23 +48,26 @@ SHARED_APPS = [
'crispy_bootstrap5',
'csp',
'djstripe', # Stripe integration
'tickets', # Ticket system - shared for platform support access
'notifications', # Notification system - shared for platform to notify tenants
'smoothschedule.public_api', # Public API v1 for third-party integrations
'smoothschedule.comms_credits', # Communication credits (SMS/calling) - shared for billing
'smoothschedule.field_mobile', # Field employee mobile app - shared for location tracking
# Commerce Domain (shared for platform support)
'smoothschedule.commerce.tickets', # Ticket system - shared for platform support access
# Communication Domain (shared)
'smoothschedule.communication.notifications', # Notification system - shared for platform
'smoothschedule.communication.credits', # Communication credits (SMS/calling) - shared for billing
'smoothschedule.communication.mobile', # Field employee mobile app - shared for location tracking
]
# Tenant-specific apps - Each tenant gets isolated data in their own schema
TENANT_APPS = [
'django.contrib.contenttypes', # Needed for tenant schemas
'schedule', # Resource scheduling with configurable concurrency
'payments', # Stripe Connect payments bridge
'contracts', # Contract/e-signature system
# Add your tenant-scoped business logic apps here:
# 'appointments',
# 'customers',
# 'analytics',
# Scheduling Domain (tenant-isolated)
'smoothschedule.scheduling.schedule', # Resource scheduling with configurable concurrency
'smoothschedule.scheduling.contracts', # Contract/e-signature system
# Commerce Domain (tenant-isolated)
'smoothschedule.commerce.payments', # Stripe Connect payments bridge
]
@@ -96,7 +102,7 @@ MIDDLEWARE = [
# 1. Tenant resolution
'django_tenants.middleware.main.TenantMainMiddleware',
'core.middleware.TenantHeaderMiddleware', # Support tenant switching via header
'smoothschedule.identity.core.middleware.TenantHeaderMiddleware', # Support tenant switching via header
# 2. Security middleware
'django.middleware.security.SecurityMiddleware',
@@ -108,7 +114,7 @@ MIDDLEWARE = [
# 4. Sandbox mode - switches to sandbox schema if requested
# MUST come after TenantMainMiddleware and SessionMiddleware
'core.middleware.SandboxModeMiddleware',
'smoothschedule.identity.core.middleware.SandboxModeMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
@@ -120,7 +126,7 @@ MIDDLEWARE = [
'hijack.middleware.HijackUserMiddleware',
# 6. MASQUERADE AUDIT - MUST come AFTER HijackUserMiddleware
'core.middleware.MasqueradeAuditMiddleware',
'smoothschedule.identity.core.middleware.MasqueradeAuditMiddleware',
# 7. Messages, Clickjacking, and Allauth
'django.contrib.messages.middleware.MessageMiddleware',
@@ -176,7 +182,7 @@ AUTH_PASSWORD_VALIDATORS = [
# HIJACK (MASQUERADING) CONFIGURATION
# =============================================================================
HIJACK_AUTHORIZATION_CHECK = 'core.permissions.can_hijack'
HIJACK_AUTHORIZATION_CHECK = 'smoothschedule.identity.core.permissions.can_hijack'
HIJACK_DISPLAY_ADMIN_BUTTON = True
HIJACK_USE_BOOTSTRAP = True
HIJACK_ALLOW_GET_REQUESTS = False # Security: require POST

View File

@@ -2,9 +2,8 @@
With these settings, tests run faster.
"""
from .base import * # noqa: F403
from .base import TEMPLATES
from .base import env
from .multitenancy import * # noqa: F403
from .multitenancy import TEMPLATES, env
# GENERAL
# ------------------------------------------------------------------------------

View File

@@ -10,33 +10,33 @@ from drf_spectacular.views import SpectacularAPIView
from drf_spectacular.views import SpectacularSwaggerView
from rest_framework.authtoken.views import obtain_auth_token
from smoothschedule.users.api_views import (
from smoothschedule.identity.users.api_views import (
login_view, current_user_view, logout_view, send_verification_email, verify_email,
hijack_acquire_view, hijack_release_view,
staff_invitations_view, cancel_invitation_view, resend_invitation_view,
invitation_details_view, accept_invitation_view, decline_invitation_view,
check_subdomain_view, signup_view
)
from smoothschedule.users.mfa_api_views import (
from smoothschedule.identity.users.mfa_api_views import (
mfa_status, send_phone_verification, verify_phone, enable_sms_mfa,
setup_totp, verify_totp_setup, generate_backup_codes, backup_codes_status,
disable_mfa, mfa_login_send_code, mfa_login_verify,
list_trusted_devices, revoke_trusted_device, revoke_all_trusted_devices
)
from schedule.api_views import (
from smoothschedule.scheduling.schedule.api_views import (
current_business_view, update_business_view,
oauth_settings_view, oauth_credentials_view,
custom_domains_view, custom_domain_detail_view,
custom_domain_verify_view, custom_domain_set_primary_view,
sandbox_status_view, sandbox_toggle_view, sandbox_reset_view
)
from core.email_autoconfig import (
from smoothschedule.identity.core.email_autoconfig import (
MozillaAutoconfigView,
MicrosoftAutodiscoverView,
AppleConfigProfileView,
WellKnownAutoconfigView,
)
from core.api_views import (
from smoothschedule.identity.core.api_views import (
quota_status_view,
quota_resources_view,
quota_archive_view,
@@ -48,7 +48,7 @@ urlpatterns = [
# Django Admin, use {% url 'admin:index' %}
path(settings.ADMIN_URL, admin.site.urls),
# User management
path("users/", include("smoothschedule.users.urls", namespace="users")),
path("users/", include("smoothschedule.identity.users.urls", namespace="users")),
path("accounts/", include("allauth.urls")),
# Django Hijack (masquerade) - for admin interface
path("hijack/", include("hijack.urls")),
@@ -78,28 +78,28 @@ urlpatterns += [
# Stripe Webhooks (dj-stripe built-in handler)
path("stripe/", include("djstripe.urls", namespace="djstripe")),
# Public API v1 (for third-party integrations)
path("v1/", include("smoothschedule.public_api.urls", namespace="public_api")),
path("v1/", include("smoothschedule.platform.api.urls", namespace="public_api")),
# Schedule API (internal)
path("", include("schedule.urls")),
path("", include("smoothschedule.scheduling.schedule.urls")),
# Analytics API
path("", include("analytics.urls")),
path("", include("smoothschedule.scheduling.analytics.urls")),
# Payments API
path("payments/", include("payments.urls")),
path("payments/", include("smoothschedule.commerce.payments.urls")),
# Contracts API
path("contracts/", include("contracts.urls")),
path("contracts/", include("smoothschedule.scheduling.contracts.urls")),
# Communication Credits API
path("communication-credits/", include("smoothschedule.comms_credits.urls", namespace="comms_credits")),
path("communication-credits/", include("smoothschedule.communication.credits.urls", namespace="comms_credits")),
# Field Mobile API (for field employee mobile app)
path("mobile/", include("smoothschedule.field_mobile.urls", namespace="field_mobile")),
path("mobile/", include("smoothschedule.communication.mobile.urls", namespace="field_mobile")),
# Tickets API
path("tickets/", include("tickets.urls")),
path("tickets/", include("smoothschedule.commerce.tickets.urls")),
# Notifications API
path("notifications/", include("notifications.urls")),
path("notifications/", include("smoothschedule.communication.notifications.urls")),
# Platform API
path("platform/", include("platform_admin.urls", namespace="platform")),
path("platform/", include("smoothschedule.platform.admin.urls", namespace="platform")),
# OAuth Email Integration API
path("oauth/", include("core.oauth_urls", namespace="oauth")),
path("auth/oauth/", include("core.oauth_urls", namespace="auth_oauth")),
path("oauth/", include("smoothschedule.identity.core.oauth_urls", namespace="oauth")),
path("auth/oauth/", include("smoothschedule.identity.core.oauth_urls", namespace="auth_oauth")),
# Auth API
path("auth-token/", csrf_exempt(obtain_auth_token), name="obtain_auth_token"),
path("auth/signup/check-subdomain/", check_subdomain_view, name="check_subdomain"),

View File

@@ -6,7 +6,7 @@ import django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
django.setup()
from smoothschedule.users.models import User
from smoothschedule.identity.users.models import User
from rest_framework.authtoken.models import Token
# Create or get a superuser with platform admin role

View File

@@ -1,7 +1,7 @@
"""
Create a default tenant for local development
"""
from core.models import Tenant, Domain
from smoothschedule.identity.core.models import Tenant, Domain
from django.contrib.auth import get_user_model
User = get_user_model()

View File

@@ -3,7 +3,7 @@ import os
import django
from django.conf import settings
from django_tenants.utils import tenant_context
from core.models import Tenant
from smoothschedule.identity.core.models import Tenant
# Setup Django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")

View File

@@ -2,7 +2,7 @@
Script to ensure production domain exists in the database.
Run with: python manage.py shell < scripts/ensure_production_domain.py
"""
from core.models import Tenant, Domain
from smoothschedule.identity.core.models import Tenant, Domain
from django.conf import settings
def ensure_production_domain():

View File

@@ -3,4 +3,5 @@ from django.apps import AppConfig
class PaymentsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'payments'
name = 'smoothschedule.commerce.payments'
label = 'payments'

View File

@@ -92,6 +92,7 @@ class TransactionLink(models.Model):
)
class Meta:
app_label = 'payments'
ordering = ['-created_at']
indexes = [
models.Index(fields=['status', 'created_at']),

View File

@@ -10,13 +10,13 @@ from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework import status
from core.permissions import HasFeaturePermission
from core.mixins import TenantAPIView, TenantRequiredAPIView
from smoothschedule.identity.core.permissions import HasFeaturePermission
from smoothschedule.identity.core.mixins import TenantAPIView, TenantRequiredAPIView
from decimal import Decimal
from .services import get_stripe_service_for_tenant
from .models import TransactionLink
from schedule.models import Event
from platform_admin.models import SubscriptionPlan
from smoothschedule.scheduling.schedule.models import Event
from smoothschedule.platform.admin.models import SubscriptionPlan
# ============================================================================
@@ -1532,8 +1532,8 @@ class CustomerBillingView(APIView):
def get(self, request):
"""Get customer billing data."""
from django.contrib.contenttypes.models import ContentType
from schedule.models import Participant
from smoothschedule.users.models import User
from smoothschedule.scheduling.schedule.models import Participant
from smoothschedule.identity.users.models import User
user = request.user
@@ -1653,7 +1653,7 @@ class CustomerPaymentMethodsView(APIView):
def get(self, request):
"""Get customer's saved payment methods from Stripe."""
from smoothschedule.users.models import User
from smoothschedule.identity.users.models import User
user = request.user
@@ -1726,7 +1726,7 @@ class CustomerSetupIntentView(APIView):
"""Create a SetupIntent for the customer."""
import logging
logger = logging.getLogger(__name__)
from smoothschedule.users.models import User
from smoothschedule.identity.users.models import User
user = request.user
tenant = request.tenant
@@ -1841,7 +1841,7 @@ class CustomerPaymentMethodDeleteView(APIView):
def delete(self, request, payment_method_id):
"""Delete a payment method."""
from smoothschedule.users.models import User
from smoothschedule.identity.users.models import User
user = request.user
@@ -1904,7 +1904,7 @@ class CustomerPaymentMethodDefaultView(APIView):
def post(self, request, payment_method_id):
"""Set payment method as default."""
from smoothschedule.users.models import User
from smoothschedule.identity.users.models import User
user = request.user
@@ -1989,8 +1989,8 @@ class SetFinalPriceView(APIView):
import logging
logger = logging.getLogger(__name__)
from django.contrib.contenttypes.models import ContentType
from schedule.models import Participant
from smoothschedule.users.models import User
from smoothschedule.scheduling.schedule.models import Participant
from smoothschedule.identity.users.models import User
final_price = request.data.get('final_price')
charge_now = request.data.get('charge_now', True)

View File

@@ -7,7 +7,7 @@ from django.dispatch import receiver
from djstripe import signals
from django.utils import timezone
from .models import TransactionLink
from schedule.models import Event
from smoothschedule.scheduling.schedule.models import Event
import logging
logger = logging.getLogger(__name__)

View File

@@ -3,7 +3,8 @@ from django.apps import AppConfig
class TicketsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'tickets'
name = 'smoothschedule.commerce.tickets'
label = 'tickets'
def ready(self):
import tickets.signals # noqa
import smoothschedule.commerce.tickets.signals # noqa

View File

@@ -2,7 +2,7 @@ import json
from channels.generic.websocket import AsyncWebsocketConsumer
from asgiref.sync import sync_to_async
from smoothschedule.users.models import User
from smoothschedule.identity.users.models import User
from .models import Ticket, TicketComment
from .serializers import TicketSerializer, TicketCommentSerializer # Import your serializers

View File

@@ -31,7 +31,7 @@ def get_default_platform_email():
Returns None if no default is configured.
"""
try:
from platform_admin.models import PlatformEmailAddress
from smoothschedule.platform.admin.models import PlatformEmailAddress
return PlatformEmailAddress.objects.filter(
is_default=True,
is_active=True,
@@ -75,7 +75,7 @@ class TicketEmailService:
Returns None if template not found.
"""
try:
from schedule.models import EmailTemplate
from smoothschedule.scheduling.schedule.models import EmailTemplate
return EmailTemplate.objects.filter(
name=template_name,
scope=EmailTemplate.Scope.BUSINESS

View File

@@ -37,7 +37,7 @@ from .models import (
TicketEmailAddress,
IncomingTicketEmail
)
from smoothschedule.users.models import User
from smoothschedule.identity.users.models import User
logger = logging.getLogger(__name__)
@@ -713,7 +713,7 @@ class PlatformEmailReceiver:
def __init__(self, email_address):
"""Initialize with a PlatformEmailAddress instance."""
from platform_admin.models import PlatformEmailAddress
from smoothschedule.platform.admin.models import PlatformEmailAddress
self.email_address = email_address
self.connection = None

View File

@@ -1,8 +1,8 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from core.models import Tenant
from smoothschedule.users.models import User
from smoothschedule.identity.core.models import Tenant
from smoothschedule.identity.users.models import User
class Ticket(models.Model):
@@ -160,6 +160,7 @@ class Ticket(models.Model):
resolved_at = models.DateTimeField(null=True, blank=True)
class Meta:
app_label = 'tickets'
ordering = ['-priority', '-created_at']
indexes = [
models.Index(fields=['tenant', 'status']),
@@ -247,6 +248,7 @@ class TicketTemplate(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
app_label = 'tickets'
ordering = ['ticket_type', 'name']
def __str__(self):
@@ -285,6 +287,7 @@ class CannedResponse(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
app_label = 'tickets'
ordering = ['-use_count', 'title']
def __str__(self):
@@ -349,6 +352,7 @@ class TicketComment(models.Model):
)
class Meta:
app_label = 'tickets'
ordering = ['created_at']
@property
@@ -495,6 +499,7 @@ class IncomingTicketEmail(models.Model):
)
class Meta:
app_label = 'tickets'
ordering = ['-received_at']
indexes = [
models.Index(fields=['message_id']),
@@ -640,6 +645,7 @@ class TicketEmailAddress(models.Model):
updated_at = models.DateTimeField(auto_now=True)
class Meta:
app_label = 'tickets'
ordering = ['-is_default', 'display_name']
unique_together = [['tenant', 'email_address']]
indexes = [

View File

@@ -1,7 +1,7 @@
from rest_framework import serializers
from .models import Ticket, TicketComment, TicketTemplate, CannedResponse, IncomingTicketEmail, TicketEmailAddress
from smoothschedule.users.models import User
from core.models import Tenant
from smoothschedule.identity.users.models import User
from smoothschedule.identity.core.models import Tenant
class TicketCommentSerializer(serializers.ModelSerializer):
author_email = serializers.ReadOnlyField(source='author.email')

View File

@@ -8,7 +8,7 @@ from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
from .models import Ticket, TicketComment
from smoothschedule.users.models import User
from smoothschedule.identity.users.models import User
logger = logging.getLogger(__name__)
@@ -25,7 +25,7 @@ def is_notifications_available():
global _notifications_available
if _notifications_available is None:
try:
from notifications.models import Notification
from smoothschedule.communication.notifications.models import Notification
# Check if the table exists by doing a simple query
Notification.objects.exists()
_notifications_available = True
@@ -60,7 +60,7 @@ def create_notification(recipient, actor, verb, action_object, target, data):
return
try:
from notifications.models import Notification
from smoothschedule.communication.notifications.models import Notification
Notification.objects.create(
recipient=recipient,
actor=actor,

View File

@@ -33,7 +33,7 @@ def fetch_incoming_emails(self):
"""
from .email_receiver import TicketEmailReceiver, PlatformEmailReceiver
from .models import TicketEmailAddress
from platform_admin.models import PlatformEmailAddress
from smoothschedule.platform.admin.models import PlatformEmailAddress
total_processed = 0
results = []

View File

@@ -7,8 +7,8 @@ from rest_framework.views import APIView
from django.db.models import Q
from rest_framework.filters import OrderingFilter, SearchFilter
from core.models import Tenant
from smoothschedule.users.models import User
from smoothschedule.identity.core.models import Tenant
from smoothschedule.identity.users.models import User
from .models import Ticket, TicketComment, TicketTemplate, CannedResponse, IncomingTicketEmail, TicketEmailAddress
from .serializers import (
TicketSerializer, TicketListSerializer, TicketCommentSerializer,
@@ -941,7 +941,7 @@ class RefreshTicketEmailsView(APIView):
)
from .email_receiver import PlatformEmailReceiver
from platform_admin.models import PlatformEmailAddress
from smoothschedule.platform.admin.models import PlatformEmailAddress
results = []
total_processed = 0

View File

@@ -3,5 +3,6 @@ from django.apps import AppConfig
class CommsCreditsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'smoothschedule.comms_credits'
name = 'smoothschedule.communication.credits'
label = 'comms_credits'
verbose_name = 'Communication Credits'

View File

@@ -109,6 +109,7 @@ class CommunicationCredits(models.Model):
updated_at = models.DateTimeField(auto_now=True)
class Meta:
app_label = 'comms_credits'
verbose_name = 'Communication Credits'
verbose_name_plural = 'Communication Credits'
@@ -210,7 +211,7 @@ class CommunicationCredits(models.Model):
def _send_low_balance_warning(self):
"""Send low balance warning email."""
from smoothschedule.comms_credits.tasks import send_low_balance_warning
from smoothschedule.communication.credits.tasks import send_low_balance_warning
send_low_balance_warning.delay(self.id)
self.low_balance_warning_sent = True
@@ -219,7 +220,7 @@ class CommunicationCredits(models.Model):
def _trigger_auto_reload(self):
"""Trigger auto-reload of credits."""
from smoothschedule.comms_credits.tasks import process_auto_reload
from smoothschedule.communication.credits.tasks import process_auto_reload
process_auto_reload.delay(self.id)
@@ -291,6 +292,7 @@ class CreditTransaction(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
app_label = 'comms_credits'
ordering = ['-created_at']
indexes = [
models.Index(fields=['credits', '-created_at']),
@@ -383,6 +385,7 @@ class ProxyPhoneNumber(models.Model):
updated_at = models.DateTimeField(auto_now=True)
class Meta:
app_label = 'comms_credits'
ordering = ['phone_number']
verbose_name = 'Proxy Phone Number'
verbose_name_plural = 'Proxy Phone Numbers'
@@ -495,6 +498,7 @@ class MaskedSession(models.Model):
updated_at = models.DateTimeField(auto_now=True)
class Meta:
app_label = 'comms_credits'
ordering = ['-created_at']
indexes = [
models.Index(fields=['tenant', 'status']),

View File

@@ -20,7 +20,7 @@ def sync_twilio_usage_all_tenants():
2. Calculate charges with markup
3. Deduct from tenant credits
"""
from core.models import Tenant
from smoothschedule.identity.core.models import Tenant
tenants = Tenant.objects.exclude(twilio_subaccount_sid='')
@@ -46,7 +46,7 @@ def sync_twilio_usage_for_tenant(tenant_id):
Fetches usage from Twilio API and deducts from credits.
"""
from core.models import Tenant
from smoothschedule.identity.core.models import Tenant
from .models import CommunicationCredits
try:
@@ -219,7 +219,7 @@ def process_auto_reload(credits_id):
try:
# Get Stripe API key from platform settings
from platform_admin.models import PlatformSettings
from smoothschedule.platform.admin.models import PlatformSettings
platform_settings = PlatformSettings.get_instance()
stripe.api_key = platform_settings.get_stripe_secret_key()
@@ -373,7 +373,7 @@ def create_twilio_subaccount(tenant_id):
Called when SMS/calling is first enabled for a tenant.
"""
from core.models import Tenant
from smoothschedule.identity.core.models import Tenant
from twilio.rest import Client
try:

View File

@@ -3,4 +3,5 @@ from django.apps import AppConfig
class CommunicationConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'communication'
name = 'smoothschedule.communication.messaging'
label = 'communication'

View File

@@ -83,6 +83,7 @@ class CommunicationSession(models.Model):
)
class Meta:
app_label = 'communication'
ordering = ['-created_at']
indexes = [
models.Index(fields=['is_active', 'created_at']),

View File

@@ -57,7 +57,7 @@ class TwilioService:
PermissionError: If tenant doesn't have masked calling feature
"""
from django.db import connection
from core.models import Tenant
from smoothschedule.identity.core.models import Tenant
from rest_framework.exceptions import PermissionDenied
# Check feature permission

View File

@@ -3,7 +3,7 @@ from django.apps import AppConfig
class FieldMobileConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'smoothschedule.field_mobile'
name = 'smoothschedule.communication.mobile'
label = 'field_mobile'
verbose_name = 'Field Mobile App'

View File

@@ -81,6 +81,7 @@ class EventStatusHistory(models.Model):
)
class Meta:
app_label = 'field_mobile'
ordering = ['-changed_at']
indexes = [
models.Index(fields=['tenant', 'event_id']),
@@ -174,6 +175,7 @@ class EmployeeLocationUpdate(models.Model):
)
class Meta:
app_label = 'field_mobile'
ordering = ['-timestamp']
indexes = [
models.Index(fields=['tenant', 'event_id', '-timestamp']),
@@ -315,6 +317,7 @@ class FieldCallLog(models.Model):
ended_at = models.DateTimeField(null=True, blank=True)
class Meta:
app_label = 'field_mobile'
ordering = ['-initiated_at']
indexes = [
models.Index(fields=['tenant', 'event_id']),

View File

@@ -6,8 +6,8 @@ Serializers for the field employee mobile app API.
from rest_framework import serializers
from django.utils import timezone
from schedule.models import Event, Service, Participant
from smoothschedule.field_mobile.models import (
from smoothschedule.scheduling.schedule.models import Event, Service, Participant
from smoothschedule.communication.mobile.models import (
EventStatusHistory,
EmployeeLocationUpdate,
FieldCallLog,
@@ -110,7 +110,7 @@ class JobListSerializer(serializers.ModelSerializer):
def get_allowed_transitions(self, obj):
"""Get list of statuses this job can transition to."""
from smoothschedule.field_mobile.services import StatusMachine
from smoothschedule.communication.mobile.services import StatusMachine
# Get the valid transitions without needing user context
return StatusMachine.VALID_TRANSITIONS.get(obj.status, [])
@@ -118,7 +118,7 @@ class JobListSerializer(serializers.ModelSerializer):
def _get_customer_participant(self, obj):
"""Get the customer User from participants."""
from django.contrib.contenttypes.models import ContentType
from smoothschedule.users.models import User
from smoothschedule.identity.users.models import User
if not hasattr(self, '_customer_cache'):
self._customer_cache = {}
@@ -196,8 +196,8 @@ class JobDetailSerializer(serializers.ModelSerializer):
def get_assigned_staff(self, obj):
"""Get list of assigned staff members."""
from django.contrib.contenttypes.models import ContentType
from smoothschedule.users.models import User
from schedule.models import Resource
from smoothschedule.identity.users.models import User
from smoothschedule.scheduling.schedule.models import Resource
staff = []
@@ -239,12 +239,12 @@ class JobDetailSerializer(serializers.ModelSerializer):
return None
def get_allowed_transitions(self, obj):
from smoothschedule.field_mobile.services import StatusMachine
from smoothschedule.communication.mobile.services import StatusMachine
return StatusMachine.VALID_TRANSITIONS.get(obj.status, [])
def get_can_track_location(self, obj):
"""Check if location tracking is allowed for current status."""
from smoothschedule.field_mobile.services import StatusMachine
from smoothschedule.communication.mobile.services import StatusMachine
return obj.status in StatusMachine.TRACKING_STATUSES
def get_has_active_call_session(self, obj):
@@ -253,7 +253,7 @@ class JobDetailSerializer(serializers.ModelSerializer):
if not tenant:
return False
from smoothschedule.comms_credits.models import MaskedSession
from smoothschedule.communication.credits.models import MaskedSession
return MaskedSession.objects.filter(
tenant=tenant,
event_id=obj.id,
@@ -305,7 +305,7 @@ class JobDetailSerializer(serializers.ModelSerializer):
def _get_customer_participant(self, obj):
from django.contrib.contenttypes.models import ContentType
from smoothschedule.users.models import User
from smoothschedule.identity.users.models import User
try:
user_ct = ContentType.objects.get_for_model(User)
@@ -324,7 +324,7 @@ class JobDetailSerializer(serializers.ModelSerializer):
Returns True if the user's linked resource has user_can_edit_schedule=True.
"""
from django.contrib.contenttypes.models import ContentType
from schedule.models import Resource
from smoothschedule.scheduling.schedule.models import Resource
# Get the current user from context
request = self.context.get('request')

View File

@@ -8,8 +8,8 @@ from django.db import transaction
from typing import Optional, Tuple
from decimal import Decimal
from schedule.models import Event
from smoothschedule.field_mobile.models import EventStatusHistory, EmployeeLocationUpdate
from smoothschedule.scheduling.schedule.models import Event
from smoothschedule.communication.mobile.models import EventStatusHistory, EmployeeLocationUpdate
class StatusTransitionError(Exception):
@@ -124,8 +124,8 @@ class StatusMachine:
- User is linked to a Resource that is a participant
"""
from django.contrib.contenttypes.models import ContentType
from schedule.models import Participant, Resource
from smoothschedule.users.models import User
from smoothschedule.scheduling.schedule.models import Participant, Resource
from smoothschedule.identity.users.models import User
# Check if user is directly a participant
user_ct = ContentType.objects.get_for_model(User)
@@ -158,7 +158,7 @@ class StatusMachine:
Returns:
Tuple of (is_allowed, reason_if_not)
"""
from smoothschedule.users.models import User
from smoothschedule.identity.users.models import User
# Owners and managers can always change status
if self.user.role in [User.Role.TENANT_OWNER, User.Role.TENANT_MANAGER]:
@@ -235,7 +235,7 @@ class StatusMachine:
self._stop_location_tracking(event)
# Emit status change signal (triggers notifications and plugin hooks)
from schedule.signals import emit_status_change
from smoothschedule.scheduling.schedule.signals import emit_status_change
emit_status_change(
event=event,
old_status=old_status,

View File

@@ -90,7 +90,7 @@ class TwilioFieldCallService:
Returns:
MaskedSession instance
"""
from smoothschedule.comms_credits.models import MaskedSession, ProxyPhoneNumber
from smoothschedule.communication.credits.models import MaskedSession, ProxyPhoneNumber
# Check for existing active session
existing = MaskedSession.objects.filter(
@@ -145,7 +145,7 @@ class TwilioFieldCallService:
1. Numbers already assigned to this tenant
2. Numbers in the shared pool (AVAILABLE status)
"""
from smoothschedule.comms_credits.models import ProxyPhoneNumber
from smoothschedule.communication.credits.models import ProxyPhoneNumber
# First, try tenant's assigned numbers
tenant_number = ProxyPhoneNumber.objects.filter(
@@ -180,7 +180,7 @@ class TwilioFieldCallService:
Args:
estimated_cost_cents: Estimated cost of the call/SMS
"""
from smoothschedule.comms_credits.models import CommunicationCredits
from smoothschedule.communication.credits.models import CommunicationCredits
try:
credits = CommunicationCredits.objects.get(tenant=self.tenant)
@@ -202,8 +202,8 @@ class TwilioFieldCallService:
Looks up the customer participant and returns their phone.
"""
from django.contrib.contenttypes.models import ContentType
from schedule.models import Event, Participant
from smoothschedule.users.models import User
from smoothschedule.scheduling.schedule.models import Event, Participant
from smoothschedule.identity.users.models import User
from django_tenants.utils import schema_context
with schema_context(self.tenant.schema_name):
@@ -248,7 +248,7 @@ class TwilioFieldCallService:
Returns:
Dict with call_sid, proxy_number, status
"""
from smoothschedule.field_mobile.models import FieldCallLog
from smoothschedule.communication.mobile.models import FieldCallLog
# Check permissions and credits
self._check_feature_permission()
@@ -344,7 +344,7 @@ class TwilioFieldCallService:
Returns:
Dict with message_sid, status
"""
from smoothschedule.field_mobile.models import FieldCallLog
from smoothschedule.communication.mobile.models import FieldCallLog
# Check permissions and credits
self._check_feature_permission()
@@ -425,7 +425,7 @@ class TwilioFieldCallService:
Returns None if no active session exists.
"""
from smoothschedule.comms_credits.models import MaskedSession
from smoothschedule.communication.credits.models import MaskedSession
return MaskedSession.objects.filter(
tenant=self.tenant,
@@ -440,7 +440,7 @@ class TwilioFieldCallService:
Called when a job is completed to stop allowing calls/SMS.
"""
from smoothschedule.comms_credits.models import MaskedSession
from smoothschedule.communication.credits.models import MaskedSession
session = self.get_session_for_event(event_id)
if session:
@@ -458,7 +458,7 @@ class TwilioFieldCallService:
Returns:
List of FieldCallLog records
"""
from smoothschedule.field_mobile.models import FieldCallLog
from smoothschedule.communication.mobile.models import FieldCallLog
return list(
FieldCallLog.objects.filter(
@@ -507,7 +507,7 @@ def handle_incoming_call(session_id: int, from_number: str) -> str:
Returns:
TwiML response string
"""
from smoothschedule.comms_credits.models import MaskedSession
from smoothschedule.communication.credits.models import MaskedSession
from twilio.twiml.voice_response import VoiceResponse
response = VoiceResponse()
@@ -557,7 +557,7 @@ def handle_incoming_sms(session_id: int, from_number: str, body: str) -> str:
Returns:
TwiML response string (empty for SMS)
"""
from smoothschedule.comms_credits.models import MaskedSession
from smoothschedule.communication.credits.models import MaskedSession
from twilio.rest import Client
from django.conf import settings as django_settings

View File

@@ -21,11 +21,11 @@ def send_customer_status_notification(tenant_id, event_id, notification_type):
event_id: The event/job ID
notification_type: One of 'en_route_notification', 'arrived_notification', 'completed_notification'
"""
from core.models import Tenant
from smoothschedule.identity.core.models import Tenant
from django_tenants.utils import schema_context
from schedule.models import Event, Participant
from smoothschedule.scheduling.schedule.models import Event, Participant
from django.contrib.contenttypes.models import ContentType
from smoothschedule.users.models import User
from smoothschedule.identity.users.models import User
try:
tenant = Tenant.objects.get(id=tenant_id)
@@ -118,8 +118,8 @@ def send_sms_notification(tenant_id, phone_number, message):
phone_number: Recipient phone number
message: SMS message body
"""
from core.models import Tenant
from smoothschedule.comms_credits.models import CommunicationCredits
from smoothschedule.identity.core.models import Tenant
from smoothschedule.communication.credits.models import CommunicationCredits
try:
tenant = Tenant.objects.get(id=tenant_id)
@@ -190,7 +190,7 @@ def send_email_notification(tenant_id, email, subject, message, customer_name='C
message: Email body
customer_name: Customer's name for personalization
"""
from core.models import Tenant
from smoothschedule.identity.core.models import Tenant
from django.core.mail import send_mail
try:
@@ -237,7 +237,7 @@ def cleanup_old_location_data(days_to_keep=30):
Args:
days_to_keep: Number of days of data to retain (default 30)
"""
from smoothschedule.field_mobile.models import EmployeeLocationUpdate
from smoothschedule.communication.mobile.models import EmployeeLocationUpdate
cutoff = timezone.now() - timezone.timedelta(days=days_to_keep)
@@ -259,7 +259,7 @@ def cleanup_old_status_history(days_to_keep=365):
Args:
days_to_keep: Number of days of data to retain (default 365)
"""
from smoothschedule.field_mobile.models import EventStatusHistory
from smoothschedule.communication.mobile.models import EventStatusHistory
cutoff = timezone.now() - timezone.timedelta(days=days_to_keep)

View File

@@ -19,14 +19,14 @@ from rest_framework.response import Response
from django_tenants.utils import schema_context
from schedule.models import Event, Participant, Resource
from smoothschedule.users.models import User
from smoothschedule.field_mobile.models import (
from smoothschedule.scheduling.schedule.models import Event, Participant, Resource
from smoothschedule.identity.users.models import User
from smoothschedule.communication.mobile.models import (
EventStatusHistory,
EmployeeLocationUpdate,
FieldCallLog,
)
from smoothschedule.field_mobile.serializers import (
from smoothschedule.communication.mobile.serializers import (
JobListSerializer,
JobDetailSerializer,
SetStatusSerializer,
@@ -41,9 +41,9 @@ from smoothschedule.field_mobile.serializers import (
CallHistorySerializer,
EmployeeProfileSerializer,
)
from smoothschedule.field_mobile.services import StatusMachine, TwilioFieldCallService
from smoothschedule.field_mobile.services.status_machine import StatusTransitionError
from smoothschedule.field_mobile.services.twilio_calls import TwilioFieldCallError
from smoothschedule.communication.mobile.services import StatusMachine, TwilioFieldCallService
from smoothschedule.communication.mobile.services.status_machine import StatusTransitionError
from smoothschedule.communication.mobile.services.twilio_calls import TwilioFieldCallError
logger = logging.getLogger(__name__)
@@ -467,8 +467,8 @@ def location_update_view(request, job_id):
# Broadcast location update via WebSocket
# Find the resource linked to this user and broadcast to watchers
from schedule.models import Resource
from schedule.consumers import broadcast_resource_location_update
from smoothschedule.scheduling.schedule.models import Resource
from smoothschedule.scheduling.schedule.consumers import broadcast_resource_location_update
from asgiref.sync import async_to_sync
user_resources = Resource.objects.filter(user=user)
@@ -702,7 +702,7 @@ def twilio_voice_webhook(request, session_id):
Called by Twilio when a call is initiated or received.
Returns TwiML to route the call.
"""
from smoothschedule.field_mobile.services.twilio_calls import handle_incoming_call
from smoothschedule.communication.mobile.services.twilio_calls import handle_incoming_call
from_number = request.data.get('From', '')
twiml = handle_incoming_call(session_id, from_number)
@@ -774,7 +774,7 @@ def twilio_sms_webhook(request, session_id):
Forwards the SMS to the appropriate party.
"""
from smoothschedule.field_mobile.services.twilio_calls import handle_incoming_sms
from smoothschedule.communication.mobile.services.twilio_calls import handle_incoming_sms
from_number = request.data.get('From', '')
body = request.data.get('Body', '')

View File

@@ -3,4 +3,5 @@ from django.apps import AppConfig
class NotificationsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'notifications'
name = 'smoothschedule.communication.notifications'
label = 'notifications'

View File

@@ -2,7 +2,7 @@ from django.db import models
from django.utils.translation import gettext_lazy as _
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from smoothschedule.users.models import User
from smoothschedule.identity.users.models import User
class Notification(models.Model):
"""
@@ -57,6 +57,7 @@ class Notification(models.Model):
)
class Meta:
app_label = 'notifications'
ordering = ['-timestamp']
indexes = [
models.Index(fields=['recipient', 'read', 'timestamp']),

Some files were not shown because too many files have changed in this diff Show More