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:
52
CLAUDE.md
52
CLAUDE.md
@@ -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
383
PLAN_APP_REORGANIZATION.md
Normal 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)
|
||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
# -------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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'
|
||||||
@@ -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']),
|
||||||
@@ -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)
|
||||||
@@ -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__)
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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 = [
|
||||||
@@ -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')
|
||||||
@@ -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,
|
||||||
@@ -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 = []
|
||||||
@@ -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
|
||||||
@@ -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'
|
||||||
@@ -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']),
|
||||||
@@ -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:
|
||||||
@@ -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'
|
||||||
@@ -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']),
|
||||||
@@ -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
|
||||||
@@ -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'
|
||||||
|
|
||||||
@@ -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']),
|
||||||
@@ -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')
|
||||||
@@ -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,
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
|
||||||
@@ -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', '')
|
||||||
@@ -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'
|
||||||
@@ -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
Reference in New Issue
Block a user