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/types.ts` | TypeScript interfaces |
| `frontend/src/i18n/locales/en.json` | Translations | | `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 | | App | Location | Purpose |
|-----|----------|---------| |-----|----------|---------|
| `schedule` | `smoothschedule/smoothschedule/schedule/` | Resources, Events, Services | | `core` | `identity/core/` | Tenant, Domain, PermissionGrant, middleware, mixins |
| `users` | `smoothschedule/smoothschedule/users/` | Authentication, User model | | `users` | `identity/users/` | User model, authentication, MFA |
| `tenants` | `smoothschedule/smoothschedule/tenants/` | Multi-tenancy (Business model) |
| `core` | `smoothschedule/core/` | Shared mixins, permissions, middleware | ### Scheduling Domain
| `payments` | `smoothschedule/payments/` | Stripe integration, subscriptions | | App | Location | Purpose |
| `platform_admin` | `smoothschedule/platform_admin/` | Platform administration | |-----|----------|---------|
| `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 ## 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 ### Permission Classes
```python ```python
from core.mixins import DenyStaffWritePermission, DenyStaffAllAccessPermission, DenyStaffListPermission from smoothschedule.identity.core.mixins import DenyStaffWritePermission, DenyStaffAllAccessPermission, DenyStaffListPermission
class MyViewSet(ModelViewSet): class MyViewSet(ModelViewSet):
# Block write operations for staff (GET allowed) # 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 ```python
from smoothschedule.users.models import User from smoothschedule.identity.users.models import User
# Find the staff member # Find the staff member
staff = User.objects.get(email='john@example.com') staff = User.objects.get(email='john@example.com')
@@ -147,7 +173,7 @@ Then grant via: `staff.permissions['can_manage_equipment'] = True`
### QuerySet Mixins ### QuerySet Mixins
```python ```python
from core.mixins import TenantFilteredQuerySetMixin, UserTenantFilteredMixin from smoothschedule.identity.core.mixins import TenantFilteredQuerySetMixin, UserTenantFilteredMixin
# For tenant-scoped models (automatic django-tenants filtering) # For tenant-scoped models (automatic django-tenants filtering)
class ResourceViewSet(TenantFilteredQuerySetMixin, ModelViewSet): class ResourceViewSet(TenantFilteredQuerySetMixin, ModelViewSet):
@@ -166,7 +192,7 @@ class CustomerViewSet(UserTenantFilteredMixin, ModelViewSet):
### Feature Permission Mixins ### Feature Permission Mixins
```python ```python
from core.mixins import PluginFeatureRequiredMixin, TaskFeatureRequiredMixin from smoothschedule.identity.core.mixins import PluginFeatureRequiredMixin, TaskFeatureRequiredMixin
# Checks can_use_plugins feature on list/retrieve/create # Checks can_use_plugins feature on list/retrieve/create
class PluginViewSet(PluginFeatureRequiredMixin, ModelViewSet): class PluginViewSet(PluginFeatureRequiredMixin, ModelViewSet):
@@ -181,7 +207,7 @@ class ScheduledTaskViewSet(TaskFeatureRequiredMixin, TenantFilteredQuerySetMixin
```python ```python
from rest_framework.views import APIView 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() # Optional tenant - use self.get_tenant()
class MyView(TenantAPIView, APIView): 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 django.test import TestCase, RequestFactory
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from core.models import Tenant from core.models import Tenant
from smoothschedule.users.models import User from smoothschedule.identity.users.models import User
class CalendarSyncTests(APITestCase): class CalendarSyncTests(APITestCase):
def setUp(self): def setUp(self):

View File

@@ -95,23 +95,38 @@ smoothschedule/
│ └── traefik/ │ └── traefik/
``` ```
### Django Apps ### Django Apps (Domain-Based Organization)
``` ```
smoothschedule/smoothschedule/ smoothschedule/smoothschedule/
├── users/ # User management, authentication ├── identity/ # Identity Domain
│ ├── models.py # User model with roles │ ├── core/ # Tenant, Domain, middleware, mixins
│ ├── api_views.py # Auth endpoints, user API │ ├── models.py # Tenant, Domain, PermissionGrant
└── migrations/ │ ├── middleware.py # TenantHeader, Sandbox, Masquerade
├── schedule/ # Core scheduling functionality └── mixins.py # Base classes for views/viewsets
── models.py # Resource, Event, Service, Participant ── users/ # User management, authentication
├── serializers.py # DRF serializers ├── models.py # User model with roles
├── views.py # ViewSets for API ├── api_views.py # Auth endpoints
├── services.py # AvailabilityService └── mfa_api_views.py # MFA endpoints
│ └── migrations/ ├── scheduling/ # Scheduling Domain
├── tenants/ # Multi-tenancy (Business/Tenant models) │ ├── schedule/ # Core scheduling functionality
│ ├── models.py # Tenant, Domain models │ ├── models.py # Resource, Event, Service, Participant
└── migrations/ │ ├── 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 ### API Endpoints
@@ -160,16 +175,20 @@ You're trying to run Python directly instead of through Docker. Use `docker comp
## Key Models ## Key Models
### Resource (schedule/models.py) ### Resource (scheduling/schedule/models.py)
- `name`, `type` (STAFF/ROOM/EQUIPMENT) - `name`, `type` (STAFF/ROOM/EQUIPMENT)
- `max_concurrent_events` - concurrency limit (1=exclusive, >1=multilane, 0=unlimited) - `max_concurrent_events` - concurrency limit (1=exclusive, >1=multilane, 0=unlimited)
- `saved_lane_count` - remembers lane count when multilane disabled - `saved_lane_count` - remembers lane count when multilane disabled
- `buffer_duration` - time between events - `buffer_duration` - time between events
### Event (schedule/models.py) ### Event (scheduling/schedule/models.py)
- `title`, `start_time`, `end_time`, `status` - `title`, `start_time`, `end_time`, `status`
- Links to resources/customers via `Participant` model - 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` - Roles: `superuser`, `platform_manager`, `platform_support`, `owner`, `manager`, `staff`, `resource`, `customer`
- `business_subdomain` - which tenant they belong to - `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: In Django shell or admin, create users with different roles:
```python ```python
from smoothschedule.users.models import User from smoothschedule.identity.users.models import User
from core.models import Tenant from core.models import Tenant
# Get the 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 DefaultRouter
from rest_framework.routers import SimpleRouter 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() 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.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter from channels.routing import ProtocolTypeRouter, URLRouter
from tickets import routing as tickets_routing from smoothschedule.commerce.tickets import routing as tickets_routing
from schedule import routing as schedule_routing from smoothschedule.scheduling.schedule import routing as schedule_routing
from tickets.middleware import TokenAuthMiddleware from smoothschedule.commerce.tickets.middleware import TokenAuthMiddleware
application = ProtocolTypeRouter( application = ProtocolTypeRouter(

View File

@@ -97,17 +97,28 @@ THIRD_PARTY_APPS = [
] ]
LOCAL_APPS = [ LOCAL_APPS = [
"smoothschedule.users", # Identity Domain
"core", "smoothschedule.identity.users",
"schedule", "smoothschedule.identity.core",
"analytics",
"payments", # Scheduling Domain
"platform_admin.apps.PlatformAdminConfig", "smoothschedule.scheduling.schedule",
"notifications", # New: Generic notification app "smoothschedule.scheduling.contracts",
"tickets", # New: Support tickets app "smoothschedule.scheduling.analytics",
"smoothschedule.comms_credits", # Communication credits and SMS/calling
"smoothschedule.field_mobile", # Field employee mobile app # Communication Domain
# Your stuff: custom apps go here "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 # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
@@ -183,7 +194,7 @@ TEMPLATES = [
"django.template.context_processors.media", "django.template.context_processors.media",
"django.template.context_processors.static", "django.template.context_processors.static",
"django.template.context_processors.tz", "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 # https://docs.allauth.org/en/latest/account/configuration.html
ACCOUNT_EMAIL_VERIFICATION = "mandatory" ACCOUNT_EMAIL_VERIFICATION = "mandatory"
# https://docs.allauth.org/en/latest/account/configuration.html # 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 # 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 # 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 # 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 # 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 - Available to all tenants (stored in 'public' schema)
SHARED_APPS = [ SHARED_APPS = [
'django_tenants', # Must be first '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.contenttypes',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.sessions', 'django.contrib.sessions',
@@ -25,15 +31,12 @@ SHARED_APPS = [
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django.contrib.admin', 'django.contrib.admin',
# Users app (shared across tenants)
'smoothschedule.users',
# Third-party apps that should be shared # Third-party apps that should be shared
'rest_framework', 'rest_framework',
'rest_framework.authtoken', 'rest_framework.authtoken',
'corsheaders', 'corsheaders',
'drf_spectacular', 'drf_spectacular',
'channels', # WebSockets 'channels', # WebSockets
'allauth', 'allauth',
'allauth.account', 'allauth.account',
'allauth.mfa', 'allauth.mfa',
@@ -45,23 +48,26 @@ SHARED_APPS = [
'crispy_bootstrap5', 'crispy_bootstrap5',
'csp', 'csp',
'djstripe', # Stripe integration 'djstripe', # Stripe integration
'tickets', # Ticket system - shared for platform support access
'notifications', # Notification system - shared for platform to notify tenants # Commerce Domain (shared for platform support)
'smoothschedule.public_api', # Public API v1 for third-party integrations 'smoothschedule.commerce.tickets', # Ticket system - shared for platform support access
'smoothschedule.comms_credits', # Communication credits (SMS/calling) - shared for billing
'smoothschedule.field_mobile', # Field employee mobile app - shared for location tracking # 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-specific apps - Each tenant gets isolated data in their own schema
TENANT_APPS = [ TENANT_APPS = [
'django.contrib.contenttypes', # Needed for tenant schemas 'django.contrib.contenttypes', # Needed for tenant schemas
'schedule', # Resource scheduling with configurable concurrency
'payments', # Stripe Connect payments bridge # Scheduling Domain (tenant-isolated)
'contracts', # Contract/e-signature system 'smoothschedule.scheduling.schedule', # Resource scheduling with configurable concurrency
# Add your tenant-scoped business logic apps here: 'smoothschedule.scheduling.contracts', # Contract/e-signature system
# 'appointments',
# 'customers', # Commerce Domain (tenant-isolated)
# 'analytics', 'smoothschedule.commerce.payments', # Stripe Connect payments bridge
] ]
@@ -96,7 +102,7 @@ MIDDLEWARE = [
# 1. Tenant resolution # 1. Tenant resolution
'django_tenants.middleware.main.TenantMainMiddleware', '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 # 2. Security middleware
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
@@ -108,7 +114,7 @@ MIDDLEWARE = [
# 4. Sandbox mode - switches to sandbox schema if requested # 4. Sandbox mode - switches to sandbox schema if requested
# MUST come after TenantMainMiddleware and SessionMiddleware # MUST come after TenantMainMiddleware and SessionMiddleware
'core.middleware.SandboxModeMiddleware', 'smoothschedule.identity.core.middleware.SandboxModeMiddleware',
'django.middleware.locale.LocaleMiddleware', 'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
@@ -120,7 +126,7 @@ MIDDLEWARE = [
'hijack.middleware.HijackUserMiddleware', 'hijack.middleware.HijackUserMiddleware',
# 6. MASQUERADE AUDIT - MUST come AFTER HijackUserMiddleware # 6. MASQUERADE AUDIT - MUST come AFTER HijackUserMiddleware
'core.middleware.MasqueradeAuditMiddleware', 'smoothschedule.identity.core.middleware.MasqueradeAuditMiddleware',
# 7. Messages, Clickjacking, and Allauth # 7. Messages, Clickjacking, and Allauth
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
@@ -176,7 +182,7 @@ AUTH_PASSWORD_VALIDATORS = [
# HIJACK (MASQUERADING) CONFIGURATION # 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_DISPLAY_ADMIN_BUTTON = True
HIJACK_USE_BOOTSTRAP = True HIJACK_USE_BOOTSTRAP = True
HIJACK_ALLOW_GET_REQUESTS = False # Security: require POST HIJACK_ALLOW_GET_REQUESTS = False # Security: require POST

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
""" """
Create a default tenant for local development 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 from django.contrib.auth import get_user_model
User = get_user_model() User = get_user_model()

View File

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

View File

@@ -2,7 +2,7 @@
Script to ensure production domain exists in the database. Script to ensure production domain exists in the database.
Run with: python manage.py shell < scripts/ensure_production_domain.py 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 from django.conf import settings
def ensure_production_domain(): def ensure_production_domain():

View File

@@ -3,4 +3,5 @@ from django.apps import AppConfig
class PaymentsConfig(AppConfig): class PaymentsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' 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: class Meta:
app_label = 'payments'
ordering = ['-created_at'] ordering = ['-created_at']
indexes = [ indexes = [
models.Index(fields=['status', 'created_at']), 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.response import Response
from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework import status from rest_framework import status
from core.permissions import HasFeaturePermission from smoothschedule.identity.core.permissions import HasFeaturePermission
from core.mixins import TenantAPIView, TenantRequiredAPIView from smoothschedule.identity.core.mixins import TenantAPIView, TenantRequiredAPIView
from decimal import Decimal from decimal import Decimal
from .services import get_stripe_service_for_tenant from .services import get_stripe_service_for_tenant
from .models import TransactionLink from .models import TransactionLink
from schedule.models import Event from smoothschedule.scheduling.schedule.models import Event
from platform_admin.models import SubscriptionPlan from smoothschedule.platform.admin.models import SubscriptionPlan
# ============================================================================ # ============================================================================
@@ -1532,8 +1532,8 @@ class CustomerBillingView(APIView):
def get(self, request): def get(self, request):
"""Get customer billing data.""" """Get customer billing data."""
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from schedule.models import Participant from smoothschedule.scheduling.schedule.models import Participant
from smoothschedule.users.models import User from smoothschedule.identity.users.models import User
user = request.user user = request.user
@@ -1653,7 +1653,7 @@ class CustomerPaymentMethodsView(APIView):
def get(self, request): def get(self, request):
"""Get customer's saved payment methods from Stripe.""" """Get customer's saved payment methods from Stripe."""
from smoothschedule.users.models import User from smoothschedule.identity.users.models import User
user = request.user user = request.user
@@ -1726,7 +1726,7 @@ class CustomerSetupIntentView(APIView):
"""Create a SetupIntent for the customer.""" """Create a SetupIntent for the customer."""
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
from smoothschedule.users.models import User from smoothschedule.identity.users.models import User
user = request.user user = request.user
tenant = request.tenant tenant = request.tenant
@@ -1841,7 +1841,7 @@ class CustomerPaymentMethodDeleteView(APIView):
def delete(self, request, payment_method_id): def delete(self, request, payment_method_id):
"""Delete a payment method.""" """Delete a payment method."""
from smoothschedule.users.models import User from smoothschedule.identity.users.models import User
user = request.user user = request.user
@@ -1904,7 +1904,7 @@ class CustomerPaymentMethodDefaultView(APIView):
def post(self, request, payment_method_id): def post(self, request, payment_method_id):
"""Set payment method as default.""" """Set payment method as default."""
from smoothschedule.users.models import User from smoothschedule.identity.users.models import User
user = request.user user = request.user
@@ -1989,8 +1989,8 @@ class SetFinalPriceView(APIView):
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from schedule.models import Participant from smoothschedule.scheduling.schedule.models import Participant
from smoothschedule.users.models import User from smoothschedule.identity.users.models import User
final_price = request.data.get('final_price') final_price = request.data.get('final_price')
charge_now = request.data.get('charge_now', True) charge_now = request.data.get('charge_now', True)

View File

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

View File

@@ -3,7 +3,8 @@ from django.apps import AppConfig
class TicketsConfig(AppConfig): class TicketsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'tickets' name = 'smoothschedule.commerce.tickets'
label = 'tickets'
def ready(self): 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 channels.generic.websocket import AsyncWebsocketConsumer
from asgiref.sync import sync_to_async 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 .models import Ticket, TicketComment
from .serializers import TicketSerializer, TicketCommentSerializer # Import your serializers 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. Returns None if no default is configured.
""" """
try: try:
from platform_admin.models import PlatformEmailAddress from smoothschedule.platform.admin.models import PlatformEmailAddress
return PlatformEmailAddress.objects.filter( return PlatformEmailAddress.objects.filter(
is_default=True, is_default=True,
is_active=True, is_active=True,
@@ -75,7 +75,7 @@ class TicketEmailService:
Returns None if template not found. Returns None if template not found.
""" """
try: try:
from schedule.models import EmailTemplate from smoothschedule.scheduling.schedule.models import EmailTemplate
return EmailTemplate.objects.filter( return EmailTemplate.objects.filter(
name=template_name, name=template_name,
scope=EmailTemplate.Scope.BUSINESS scope=EmailTemplate.Scope.BUSINESS

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
from rest_framework import serializers from rest_framework import serializers
from .models import Ticket, TicketComment, TicketTemplate, CannedResponse, IncomingTicketEmail, TicketEmailAddress from .models import Ticket, TicketComment, TicketTemplate, CannedResponse, IncomingTicketEmail, TicketEmailAddress
from smoothschedule.users.models import User from smoothschedule.identity.users.models import User
from core.models import Tenant from smoothschedule.identity.core.models import Tenant
class TicketCommentSerializer(serializers.ModelSerializer): class TicketCommentSerializer(serializers.ModelSerializer):
author_email = serializers.ReadOnlyField(source='author.email') 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 asgiref.sync import async_to_sync
from .models import Ticket, TicketComment from .models import Ticket, TicketComment
from smoothschedule.users.models import User from smoothschedule.identity.users.models import User
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -25,7 +25,7 @@ def is_notifications_available():
global _notifications_available global _notifications_available
if _notifications_available is None: if _notifications_available is None:
try: try:
from notifications.models import Notification from smoothschedule.communication.notifications.models import Notification
# Check if the table exists by doing a simple query # Check if the table exists by doing a simple query
Notification.objects.exists() Notification.objects.exists()
_notifications_available = True _notifications_available = True
@@ -60,7 +60,7 @@ def create_notification(recipient, actor, verb, action_object, target, data):
return return
try: try:
from notifications.models import Notification from smoothschedule.communication.notifications.models import Notification
Notification.objects.create( Notification.objects.create(
recipient=recipient, recipient=recipient,
actor=actor, actor=actor,

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,7 +20,7 @@ def sync_twilio_usage_all_tenants():
2. Calculate charges with markup 2. Calculate charges with markup
3. Deduct from tenant credits 3. Deduct from tenant credits
""" """
from core.models import Tenant from smoothschedule.identity.core.models import Tenant
tenants = Tenant.objects.exclude(twilio_subaccount_sid='') 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. 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 from .models import CommunicationCredits
try: try:
@@ -219,7 +219,7 @@ def process_auto_reload(credits_id):
try: try:
# Get Stripe API key from platform settings # 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() platform_settings = PlatformSettings.get_instance()
stripe.api_key = platform_settings.get_stripe_secret_key() 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. 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 from twilio.rest import Client
try: try:

View File

@@ -3,4 +3,5 @@ from django.apps import AppConfig
class CommunicationConfig(AppConfig): class CommunicationConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' 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: class Meta:
app_label = 'communication'
ordering = ['-created_at'] ordering = ['-created_at']
indexes = [ indexes = [
models.Index(fields=['is_active', 'created_at']), models.Index(fields=['is_active', 'created_at']),

View File

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

View File

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

View File

@@ -81,6 +81,7 @@ class EventStatusHistory(models.Model):
) )
class Meta: class Meta:
app_label = 'field_mobile'
ordering = ['-changed_at'] ordering = ['-changed_at']
indexes = [ indexes = [
models.Index(fields=['tenant', 'event_id']), models.Index(fields=['tenant', 'event_id']),
@@ -174,6 +175,7 @@ class EmployeeLocationUpdate(models.Model):
) )
class Meta: class Meta:
app_label = 'field_mobile'
ordering = ['-timestamp'] ordering = ['-timestamp']
indexes = [ indexes = [
models.Index(fields=['tenant', 'event_id', '-timestamp']), models.Index(fields=['tenant', 'event_id', '-timestamp']),
@@ -315,6 +317,7 @@ class FieldCallLog(models.Model):
ended_at = models.DateTimeField(null=True, blank=True) ended_at = models.DateTimeField(null=True, blank=True)
class Meta: class Meta:
app_label = 'field_mobile'
ordering = ['-initiated_at'] ordering = ['-initiated_at']
indexes = [ indexes = [
models.Index(fields=['tenant', 'event_id']), 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 rest_framework import serializers
from django.utils import timezone from django.utils import timezone
from schedule.models import Event, Service, Participant from smoothschedule.scheduling.schedule.models import Event, Service, Participant
from smoothschedule.field_mobile.models import ( from smoothschedule.communication.mobile.models import (
EventStatusHistory, EventStatusHistory,
EmployeeLocationUpdate, EmployeeLocationUpdate,
FieldCallLog, FieldCallLog,
@@ -110,7 +110,7 @@ class JobListSerializer(serializers.ModelSerializer):
def get_allowed_transitions(self, obj): def get_allowed_transitions(self, obj):
"""Get list of statuses this job can transition to.""" """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 # Get the valid transitions without needing user context
return StatusMachine.VALID_TRANSITIONS.get(obj.status, []) return StatusMachine.VALID_TRANSITIONS.get(obj.status, [])
@@ -118,7 +118,7 @@ class JobListSerializer(serializers.ModelSerializer):
def _get_customer_participant(self, obj): def _get_customer_participant(self, obj):
"""Get the customer User from participants.""" """Get the customer User from participants."""
from django.contrib.contenttypes.models import ContentType 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'): if not hasattr(self, '_customer_cache'):
self._customer_cache = {} self._customer_cache = {}
@@ -196,8 +196,8 @@ class JobDetailSerializer(serializers.ModelSerializer):
def get_assigned_staff(self, obj): def get_assigned_staff(self, obj):
"""Get list of assigned staff members.""" """Get list of assigned staff members."""
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from smoothschedule.users.models import User from smoothschedule.identity.users.models import User
from schedule.models import Resource from smoothschedule.scheduling.schedule.models import Resource
staff = [] staff = []
@@ -239,12 +239,12 @@ class JobDetailSerializer(serializers.ModelSerializer):
return None return None
def get_allowed_transitions(self, obj): 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, []) return StatusMachine.VALID_TRANSITIONS.get(obj.status, [])
def get_can_track_location(self, obj): def get_can_track_location(self, obj):
"""Check if location tracking is allowed for current status.""" """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 return obj.status in StatusMachine.TRACKING_STATUSES
def get_has_active_call_session(self, obj): def get_has_active_call_session(self, obj):
@@ -253,7 +253,7 @@ class JobDetailSerializer(serializers.ModelSerializer):
if not tenant: if not tenant:
return False return False
from smoothschedule.comms_credits.models import MaskedSession from smoothschedule.communication.credits.models import MaskedSession
return MaskedSession.objects.filter( return MaskedSession.objects.filter(
tenant=tenant, tenant=tenant,
event_id=obj.id, event_id=obj.id,
@@ -305,7 +305,7 @@ class JobDetailSerializer(serializers.ModelSerializer):
def _get_customer_participant(self, obj): def _get_customer_participant(self, obj):
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from smoothschedule.users.models import User from smoothschedule.identity.users.models import User
try: try:
user_ct = ContentType.objects.get_for_model(User) 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. Returns True if the user's linked resource has user_can_edit_schedule=True.
""" """
from django.contrib.contenttypes.models import ContentType 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 # Get the current user from context
request = self.context.get('request') request = self.context.get('request')

View File

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

View File

@@ -90,7 +90,7 @@ class TwilioFieldCallService:
Returns: Returns:
MaskedSession instance MaskedSession instance
""" """
from smoothschedule.comms_credits.models import MaskedSession, ProxyPhoneNumber from smoothschedule.communication.credits.models import MaskedSession, ProxyPhoneNumber
# Check for existing active session # Check for existing active session
existing = MaskedSession.objects.filter( existing = MaskedSession.objects.filter(
@@ -145,7 +145,7 @@ class TwilioFieldCallService:
1. Numbers already assigned to this tenant 1. Numbers already assigned to this tenant
2. Numbers in the shared pool (AVAILABLE status) 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 # First, try tenant's assigned numbers
tenant_number = ProxyPhoneNumber.objects.filter( tenant_number = ProxyPhoneNumber.objects.filter(
@@ -180,7 +180,7 @@ class TwilioFieldCallService:
Args: Args:
estimated_cost_cents: Estimated cost of the call/SMS estimated_cost_cents: Estimated cost of the call/SMS
""" """
from smoothschedule.comms_credits.models import CommunicationCredits from smoothschedule.communication.credits.models import CommunicationCredits
try: try:
credits = CommunicationCredits.objects.get(tenant=self.tenant) credits = CommunicationCredits.objects.get(tenant=self.tenant)
@@ -202,8 +202,8 @@ class TwilioFieldCallService:
Looks up the customer participant and returns their phone. Looks up the customer participant and returns their phone.
""" """
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from schedule.models import Event, Participant from smoothschedule.scheduling.schedule.models import Event, Participant
from smoothschedule.users.models import User from smoothschedule.identity.users.models import User
from django_tenants.utils import schema_context from django_tenants.utils import schema_context
with schema_context(self.tenant.schema_name): with schema_context(self.tenant.schema_name):
@@ -248,7 +248,7 @@ class TwilioFieldCallService:
Returns: Returns:
Dict with call_sid, proxy_number, status 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 # Check permissions and credits
self._check_feature_permission() self._check_feature_permission()
@@ -344,7 +344,7 @@ class TwilioFieldCallService:
Returns: Returns:
Dict with message_sid, status Dict with message_sid, status
""" """
from smoothschedule.field_mobile.models import FieldCallLog from smoothschedule.communication.mobile.models import FieldCallLog
# Check permissions and credits # Check permissions and credits
self._check_feature_permission() self._check_feature_permission()
@@ -425,7 +425,7 @@ class TwilioFieldCallService:
Returns None if no active session exists. 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( return MaskedSession.objects.filter(
tenant=self.tenant, tenant=self.tenant,
@@ -440,7 +440,7 @@ class TwilioFieldCallService:
Called when a job is completed to stop allowing calls/SMS. 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) session = self.get_session_for_event(event_id)
if session: if session:
@@ -458,7 +458,7 @@ class TwilioFieldCallService:
Returns: Returns:
List of FieldCallLog records List of FieldCallLog records
""" """
from smoothschedule.field_mobile.models import FieldCallLog from smoothschedule.communication.mobile.models import FieldCallLog
return list( return list(
FieldCallLog.objects.filter( FieldCallLog.objects.filter(
@@ -507,7 +507,7 @@ def handle_incoming_call(session_id: int, from_number: str) -> str:
Returns: Returns:
TwiML response string TwiML response string
""" """
from smoothschedule.comms_credits.models import MaskedSession from smoothschedule.communication.credits.models import MaskedSession
from twilio.twiml.voice_response import VoiceResponse from twilio.twiml.voice_response import VoiceResponse
response = VoiceResponse() response = VoiceResponse()
@@ -557,7 +557,7 @@ def handle_incoming_sms(session_id: int, from_number: str, body: str) -> str:
Returns: Returns:
TwiML response string (empty for SMS) 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 twilio.rest import Client
from django.conf import settings as django_settings 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 event_id: The event/job ID
notification_type: One of 'en_route_notification', 'arrived_notification', 'completed_notification' 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 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 django.contrib.contenttypes.models import ContentType
from smoothschedule.users.models import User from smoothschedule.identity.users.models import User
try: try:
tenant = Tenant.objects.get(id=tenant_id) 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 phone_number: Recipient phone number
message: SMS message body message: SMS message body
""" """
from core.models import Tenant from smoothschedule.identity.core.models import Tenant
from smoothschedule.comms_credits.models import CommunicationCredits from smoothschedule.communication.credits.models import CommunicationCredits
try: try:
tenant = Tenant.objects.get(id=tenant_id) 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 message: Email body
customer_name: Customer's name for personalization 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 from django.core.mail import send_mail
try: try:
@@ -237,7 +237,7 @@ def cleanup_old_location_data(days_to_keep=30):
Args: Args:
days_to_keep: Number of days of data to retain (default 30) 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) cutoff = timezone.now() - timezone.timedelta(days=days_to_keep)
@@ -259,7 +259,7 @@ def cleanup_old_status_history(days_to_keep=365):
Args: Args:
days_to_keep: Number of days of data to retain (default 365) 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) 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 django_tenants.utils import schema_context
from schedule.models import Event, Participant, Resource from smoothschedule.scheduling.schedule.models import Event, Participant, Resource
from smoothschedule.users.models import User from smoothschedule.identity.users.models import User
from smoothschedule.field_mobile.models import ( from smoothschedule.communication.mobile.models import (
EventStatusHistory, EventStatusHistory,
EmployeeLocationUpdate, EmployeeLocationUpdate,
FieldCallLog, FieldCallLog,
) )
from smoothschedule.field_mobile.serializers import ( from smoothschedule.communication.mobile.serializers import (
JobListSerializer, JobListSerializer,
JobDetailSerializer, JobDetailSerializer,
SetStatusSerializer, SetStatusSerializer,
@@ -41,9 +41,9 @@ from smoothschedule.field_mobile.serializers import (
CallHistorySerializer, CallHistorySerializer,
EmployeeProfileSerializer, EmployeeProfileSerializer,
) )
from smoothschedule.field_mobile.services import StatusMachine, TwilioFieldCallService from smoothschedule.communication.mobile.services import StatusMachine, TwilioFieldCallService
from smoothschedule.field_mobile.services.status_machine import StatusTransitionError from smoothschedule.communication.mobile.services.status_machine import StatusTransitionError
from smoothschedule.field_mobile.services.twilio_calls import TwilioFieldCallError from smoothschedule.communication.mobile.services.twilio_calls import TwilioFieldCallError
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -467,8 +467,8 @@ def location_update_view(request, job_id):
# Broadcast location update via WebSocket # Broadcast location update via WebSocket
# Find the resource linked to this user and broadcast to watchers # Find the resource linked to this user and broadcast to watchers
from schedule.models import Resource from smoothschedule.scheduling.schedule.models import Resource
from schedule.consumers import broadcast_resource_location_update from smoothschedule.scheduling.schedule.consumers import broadcast_resource_location_update
from asgiref.sync import async_to_sync from asgiref.sync import async_to_sync
user_resources = Resource.objects.filter(user=user) 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. Called by Twilio when a call is initiated or received.
Returns TwiML to route the call. 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', '') from_number = request.data.get('From', '')
twiml = handle_incoming_call(session_id, from_number) 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. 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', '') from_number = request.data.get('From', '')
body = request.data.get('Body', '') body = request.data.get('Body', '')

View File

@@ -3,4 +3,5 @@ from django.apps import AppConfig
class NotificationsConfig(AppConfig): class NotificationsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' 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.utils.translation import gettext_lazy as _
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from smoothschedule.users.models import User from smoothschedule.identity.users.models import User
class Notification(models.Model): class Notification(models.Model):
""" """
@@ -57,6 +57,7 @@ class Notification(models.Model):
) )
class Meta: class Meta:
app_label = 'notifications'
ordering = ['-timestamp'] ordering = ['-timestamp']
indexes = [ indexes = [
models.Index(fields=['recipient', 'read', 'timestamp']), models.Index(fields=['recipient', 'read', 'timestamp']),

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