# Multi-Tenancy Workflow Documentation
**Complete Lifecycle: Signup → Site Creation**  
**Last Updated:** December 8, 2025

---

## Table of Contents
1. [Overview](#overview)
2. [Core Models & Relationships](#core-models--relationships)
3. [Complete Workflow: Signup to Site Creation](#complete-workflow-signup-to-site-creation)
4. [Data Flow Diagram with Sample Data](#data-flow-diagram-with-sample-data)
5. [Model Details & Fields](#model-details--fields)
6. [Architectural Gaps & Issues](#architectural-gaps--issues)
7. [Recommendations](#recommendations)

---

## Overview

IGNY8 uses a **hierarchical multi-tenancy architecture** where each client account is completely isolated:

```
Account (Tenant)
  └── Plan (subscription tier)
  └── Users (team members)
  └── Sites (websites/projects)
      └── Sectors (content categories)
          └── Keywords → Clusters → Ideas → Content
```

**Key Principles:**
- **Account Isolation:** All data scoped by `account` (stored as `tenant_id` in DB)
- **Plan-Based Limits:** Accounts limited by plan (users, sites, sectors, credits)
- **Credit-Based System:** All operations consume credits (no per-feature limits)
- **Dual Subscription Models:** Legacy model in `auth` app + new model in `billing` app

---

## Core Models & Relationships

### Primary Models

| Model | Location | Purpose | Key Fields |
|-------|----------|---------|------------|
| **Account** | `auth.models` | Tenant/Organization | `plan`, `credits`, `status`, `owner` |
| **Plan** | `auth.models` | Subscription tier | `included_credits`, `max_sites`, `max_users` |
| **User** | `auth.models` | Account member | `account`, `role`, `email` |
| **Site** | `auth.models` | Website/Project | `account`, `industry`, `domain` |
| **Sector** | `auth.models` | Content category | `site`, `account`, `industry_sector` |
| **Subscription** | `auth.models` | Subscription record (LEGACY) | `account`, `stripe_subscription_id` |
| **Subscription** | `billing.models` | NEW - Not defined yet | N/A - MISSING |
| **Invoice** | `billing.models` | Billing invoices | `account`, `subscription`, `status` |
| **Payment** | `billing.models` | Payment records | `invoice`, `payment_method`, `status` |
| **CreditTransaction** | `billing.models` | Credit history | `account`, `amount`, `balance_after` |

### Relationship Map

```
┌─────────────┐
│    Plan     │ (Global, shared across all accounts)
│   (1:many)  │
└──────┬──────┘
       │
       ▼
┌─────────────────────────────────────────────────────┐
│                   Account                           │
│  - plan: FK(Plan)                                   │
│  - credits: IntegerField                            │
│  - status: CharField (trial/active/suspended)       │
│  - owner: FK(User) [nullable]                       │
│  - payment_method: CharField                        │
│  - billing_email, billing_address, tax_id           │
└──────┬──────────────────────────────────────────────┘
       │
       ├─────────────────┬──────────────────┬─────────────────┐
       │                 │                  │                 │
       ▼                 ▼                  ▼                 ▼
┌─────────────┐   ┌─────────────┐   ┌─────────────┐   ┌─────────────┐
│    User     │   │    Site     │   │Subscription │   │  Invoice    │
│  (1:many)   │   │  (1:many)   │   │   (1:1)     │   │  (1:many)   │
└──────┬──────┘   └──────┬──────┘   └─────────────┘   └─────────────┘
       │                 │
       │                 ├──────────────────┐
       │                 │                  │
       │                 ▼                  ▼
       │          ┌─────────────┐    ┌─────────────┐
       │          │   Sector    │    │  Industry   │
       │          │  (1:many)   │    │    (FK)     │
       │          └─────────────┘    └─────────────┘
       │
       └──────────────────┐
                          ▼
                   ┌─────────────┐
                   │SiteUserAccess│ (many:many bridge)
                   └─────────────┘
```

---

## Complete Workflow: Signup to Site Creation

### Phase 1: User Registration (Free Trial)

**Endpoint:** `POST /api/v1/auth/register/`

**Request Payload:**
```json
{
  "email": "john@example.com",
  "password": "SecurePass123!",
  "password_confirm": "SecurePass123!",
  "first_name": "John",
  "last_name": "Doe",
  "account_name": "John's Business",
  "plan_slug": "free"
}
```

**Backend Flow (RegisterSerializer.create()):**

```python
# Step 1: Resolve Plan
if plan_slug == 'free':
    plan = Plan.objects.get(slug='free')  # Free plan
    account_status = 'trial'
    initial_credits = plan.included_credits  # e.g., 1000 credits
else:
    # Paid plans (starter/growth/scale)
    plan = Plan.objects.get(slug=plan_slug)
    account_status = 'pending_payment'
    initial_credits = 0  # No credits until payment

# Step 2: Create User (without account first)
user = User.objects.create_user(
    username='john-doe',  # Auto-generated from email
    email='john@example.com',
    password='hashed_password',
    role='owner',
    account=None  # Set later
)

# Step 3: Generate unique slug for Account
slug = 'johns-business'  # From account_name, ensure unique

# Step 4: Create Account
account = Account.objects.create(
    name="John's Business",
    slug='johns-business',
    owner=user,  # FK to User
    plan=plan,
    credits=initial_credits,
    status=account_status,
    payment_method='bank_transfer'
)

# Step 5: Link User to Account
user.account = account
user.save()

# Step 6: Log Initial Credits (if free trial)
if initial_credits > 0:
    CreditTransaction.objects.create(
        account=account,
        transaction_type='subscription',
        amount=initial_credits,
        balance_after=initial_credits,
        description='Free plan credits from Free Trial'
    )

# Step 7: For Paid Plans - Create Subscription & Invoice
if plan_slug in ['starter', 'growth', 'scale']:
    subscription = Subscription.objects.create(
        account=account,
        plan=plan,
        status='pending_payment',
        current_period_start=now(),
        current_period_end=now() + 30 days
    )
    
    invoice = Invoice.objects.create(
        account=account,
        subscription=subscription,
        status='pending',
        total=plan.price
    )
    
    # Create default payment method
    AccountPaymentMethod.objects.create(
        account=account,
        type='bank_transfer',
        display_name='Bank Transfer (Manual)',
        is_default=True
    )
```

**Database State After Registration (Free Trial):**

```sql
-- Accounts Table
INSERT INTO igny8_tenants (name, slug, plan_id, credits, status, owner_id)
VALUES ('Johns Business', 'johns-business', 1, 1000, 'trial', 1);

-- Users Table
INSERT INTO igny8_users (email, username, role, tenant_id)
VALUES ('john@example.com', 'john-doe', 'owner', 1);

-- Credit Transactions Table
INSERT INTO igny8_credit_transactions (tenant_id, transaction_type, amount, balance_after)
VALUES (1, 'subscription', 1000, 1000);
```

---

### Phase 2: Site Creation

**Endpoint:** `POST /api/v1/auth/sites/`

**Request Payload:**
```json
{
  "name": "Tech Blog",
  "domain": "https://techblog.com",
  "industry": 2,
  "description": "A technology blog"
}
```

**Backend Flow (SiteViewSet.create()):**

```python
# Step 1: Validate Account Plan Limits
account = request.account  # From middleware
max_sites = account.plan.max_sites  # e.g., Free plan: 1 site
current_sites = Site.objects.filter(account=account, is_active=True).count()

if current_sites >= max_sites:
    raise ValidationError("Site limit reached for your plan")

# Step 2: Auto-generate slug from name
slug = 'tech-blog'  # From name, ensure unique per account

# Step 3: Validate domain format
domain = 'https://techblog.com'  # Ensure https:// prefix

# Step 4: Create Site
site = Site.objects.create(
    account=account,  # Automatically set from middleware
    name='Tech Blog',
    slug='tech-blog',
    domain='https://techblog.com',
    industry_id=2,  # FK to Industry
    status='active',
    is_active=True
)
```

**Database State After Site Creation:**

```sql
-- Sites Table
INSERT INTO igny8_sites (tenant_id, name, slug, domain, industry_id, status)
VALUES (1, 'Tech Blog', 'tech-blog', 'https://techblog.com', 2, 'active');
```

---

### Phase 3: Sector Creation

**Endpoint:** `POST /api/v1/auth/sectors/`

**Request Payload:**
```json
{
  "site": 1,
  "name": "AI & Machine Learning",
  "industry_sector": 5,
  "description": "Articles about AI and ML"
}
```

**Backend Flow (SectorViewSet.create()):**

```python
# Step 1: Validate Site belongs to Account
site = Site.objects.get(id=1, account=request.account)

# Step 2: Validate Sector Limit
max_sectors = site.get_max_sectors_limit()  # Default: 5 sectors per site
current_sectors = site.sectors.filter(is_active=True).count()

if current_sectors >= max_sectors:
    raise ValidationError("Sector limit reached for this site")

# Step 3: Validate Industry Match
industry_sector = IndustrySector.objects.get(id=5)
if industry_sector.industry != site.industry:
    raise ValidationError("Sector must belong to site's industry")

# Step 4: Auto-generate slug
slug = 'ai-machine-learning'

# Step 5: Create Sector
sector = Sector.objects.create(
    account=request.account,  # Auto-set from site
    site=site,
    industry_sector=industry_sector,
    name='AI & Machine Learning',
    slug='ai-machine-learning',
    status='active',
    is_active=True
)
```

**Database State After Sector Creation:**

```sql
-- Sectors Table
INSERT INTO igny8_sectors (tenant_id, site_id, industry_sector_id, name, slug, status)
VALUES (1, 1, 5, 'AI & Machine Learning', 'ai-machine-learning', 'active');
```

---

## Data Flow Diagram with Sample Data

### Sample Data Setup

**Plans:**
```
ID | Name       | Slug       | Price | Credits | Max Sites | Max Users
1  | Free Trial | free       | 0.00  | 1000    | 1         | 1
2  | Starter    | starter    | 29.00 | 5000    | 3         | 3
3  | Growth     | growth     | 79.00 | 15000   | 10        | 10
4  | Scale      | scale      | 199.00| 50000   | 30        | 30
```

**Industries:**
```
ID | Name          | Slug
1  | Healthcare    | healthcare
2  | Technology    | technology
3  | Finance       | finance
```

**Industry Sectors (for Technology):**
```
ID | Industry ID | Name                  | Slug
4  | 2           | Web Development       | web-development
5  | 2           | AI & Machine Learning | ai-machine-learning
6  | 2           | Cybersecurity         | cybersecurity
```

### Complete Data Flow Example

**Scenario:** New user "John Doe" signs up for free trial, creates a tech blog, and adds 2 sectors.

```
┌─────────────────────────────────────────────────────────────────┐
│ STEP 1: User Registration (Free Trial)                         │
└─────────────────────────────────────────────────────────────────┘

Request:
  POST /api/v1/auth/register/
  {
    "email": "john@techblog.com",
    "password": "SecurePass123!",
    "account_name": "Tech Blog LLC",
    "plan_slug": "free"
  }

Database Changes:
  ┌──────────────────────────────────────────────────────────────┐
  │ Table: igny8_tenants (Accounts)                              │
  ├────┬────────────────┬──────────────┬─────┬────────┬─────────┤
  │ ID │ Name           │ Slug         │ Plan│ Credits│ Status  │
  ├────┼────────────────┼──────────────┼─────┼────────┼─────────┤
  │ 1  │ Tech Blog LLC  │ tech-blog-llc│  1  │  1000  │ trial   │
  └────┴────────────────┴──────────────┴─────┴────────┴─────────┘

  ┌──────────────────────────────────────────────────────────────┐
  │ Table: igny8_users                                           │
  ├────┬────────────────────┬───────────┬────────┬──────────────┤
  │ ID │ Email              │ Username  │ Role   │ Account (FK) │
  ├────┼────────────────────┼───────────┼────────┼──────────────┤
  │ 1  │ john@techblog.com  │ john-doe  │ owner  │      1       │
  └────┴────────────────────┴───────────┴────────┴──────────────┘

  ┌──────────────────────────────────────────────────────────────┐
  │ Table: igny8_credit_transactions                             │
  ├────┬────────┬──────────────┬────────┬──────────────────────┤
  │ ID │Account │ Type         │ Amount │ Balance After        │
  ├────┼────────┼──────────────┼────────┼──────────────────────┤
  │ 1  │   1    │ subscription │  1000  │       1000           │
  └────┴────────┴──────────────┴────────┴──────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ STEP 2: Site Creation                                           │
└─────────────────────────────────────────────────────────────────┘

Request:
  POST /api/v1/auth/sites/
  {
    "name": "Tech Insights",
    "domain": "https://techinsights.com",
    "industry": 2,
    "description": "Technology news and tutorials"
  }

Validation:
  ✓ Account has 0 sites < max_sites (1) → ALLOWED
  ✓ Industry ID 2 (Technology) exists
  ✓ Domain formatted with https://

Database Changes:
  ┌──────────────────────────────────────────────────────────────────────┐
  │ Table: igny8_sites                                                   │
  ├────┬────────┬───────────────┬──────────────────────────┬─────────────┤
  │ ID │Account │ Name          │ Domain                   │ Industry(FK)│
  ├────┼────────┼───────────────┼──────────────────────────┼─────────────┤
  │ 1  │   1    │ Tech Insights │ https://techinsights.com │      2      │
  └────┴────────┴───────────────┴──────────────────────────┴─────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ STEP 3: Sector Creation (Sector 1)                             │
└─────────────────────────────────────────────────────────────────┘

Request:
  POST /api/v1/auth/sectors/
  {
    "site": 1,
    "name": "Web Development",
    "industry_sector": 4
  }

Validation:
  ✓ Site ID 1 belongs to Account 1
  ✓ Site has 0 sectors < max_sectors (5) → ALLOWED
  ✓ IndustrySector ID 4 belongs to Industry 2 (Technology) ✓

Database Changes:
  ┌──────────────────────────────────────────────────────────────────┐
  │ Table: igny8_sectors                                             │
  ├────┬────────┬──────┬──────────────────┬───────────────────────┤
  │ ID │Account │ Site │ Name             │ IndustrySector (FK)   │
  ├────┼────────┼──────┼──────────────────┼───────────────────────┤
  │ 1  │   1    │  1   │ Web Development  │          4            │
  └────┴────────┴──────┴──────────────────┴───────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ STEP 4: Sector Creation (Sector 2)                             │
└─────────────────────────────────────────────────────────────────┘

Request:
  POST /api/v1/auth/sectors/
  {
    "site": 1,
    "name": "Artificial Intelligence",
    "industry_sector": 5
  }

Validation:
  ✓ Site ID 1 belongs to Account 1
  ✓ Site has 1 sector < max_sectors (5) → ALLOWED
  ✓ IndustrySector ID 5 belongs to Industry 2 (Technology) ✓

Database Changes:
  ┌──────────────────────────────────────────────────────────────────┐
  │ Table: igny8_sectors                                             │
  ├────┬────────┬──────┬────────────────────────┬──────────────────┤
  │ ID │Account │ Site │ Name                   │ IndustrySector(FK)│
  ├────┼────────┼──────┼────────────────────────┼──────────────────┤
  │ 1  │   1    │  1   │ Web Development        │        4         │
  │ 2  │   1    │  1   │ Artificial Intelligence│        5         │
  └────┴────────┴──────┴────────────────────────┴──────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ FINAL STATE: Complete Tenancy Structure                        │
└─────────────────────────────────────────────────────────────────┘

Account: Tech Blog LLC (ID: 1)
  ├── Plan: Free Trial
  ├── Credits: 1000
  ├── Status: trial
  │
  ├── Users (1/1 max):
  │   └── john@techblog.com (owner)
  │
  └── Sites (1/1 max):
      └── Tech Insights (ID: 1)
          ├── Industry: Technology
          ├── Domain: https://techinsights.com
          │
          └── Sectors (2/5 max):
              ├── Web Development
              └── Artificial Intelligence
```

---

## Model Details & Fields

### Account Model

**Location:** `backend/igny8_core/auth/models.py`  
**Table:** `igny8_tenants`  
**Inheritance:** `SoftDeletableModel`

| Field | Type | Purpose | Notes |
|-------|------|---------|-------|
| `id` | AutoField | Primary key | Auto-increment |
| `name` | CharField(255) | Display name | "Tech Blog LLC" |
| `slug` | SlugField(255) | URL-safe identifier | Unique, auto-generated |
| `owner` | FK(User) | Account owner | Nullable, SET_NULL on delete |
| `plan` | FK(Plan) | Subscription tier | PROTECT on delete |
| `credits` | IntegerField | Available credits | Default: 0, Min: 0 |
| `status` | CharField(20) | Account state | trial/active/suspended/cancelled |
| `payment_method` | CharField(30) | Default payment | stripe/paypal/bank_transfer |
| `stripe_customer_id` | CharField(255) | Stripe reference | Nullable |
| `billing_email` | EmailField | Billing contact | Nullable |
| `billing_address_line1` | CharField(255) | Street address | Blank allowed |
| `billing_address_line2` | CharField(255) | Apt/Suite | Blank allowed |
| `billing_city` | CharField(100) | City | Blank allowed |
| `billing_state` | CharField(100) | State/Province | Blank allowed |
| `billing_postal_code` | CharField(20) | ZIP/Postal code | Blank allowed |
| `billing_country` | CharField(2) | ISO country code | Blank allowed |
| `tax_id` | CharField(100) | VAT/Tax ID | Blank allowed |
| `deletion_retention_days` | PositiveIntegerField | Soft delete retention | Default: 14, Range: 1-365 |
| `is_deleted` | BooleanField | Soft delete flag | Default: False, indexed |
| `deleted_at` | DateTimeField | Deletion timestamp | Nullable, indexed |
| `deleted_by` | FK(User) | Who deleted | Nullable, SET_NULL |
| `delete_reason` | CharField(255) | Why deleted | Nullable |
| `restore_until` | DateTimeField | Restore deadline | Nullable, indexed |
| `created_at` | DateTimeField | Creation time | Auto-set |
| `updated_at` | DateTimeField | Last update | Auto-updated |

**Key Methods:**
- `is_system_account()` - Check if slug in ['aws-admin', 'default-account']
- `soft_delete(user, reason, retention_days)` - Soft delete with retention
- `__str__()` - Returns `name`

**Indexes:**
- `slug` (unique)
- `status`

---

### Plan Model

**Location:** `backend/igny8_core/auth/models.py`  
**Table:** `igny8_plans`

| Field | Type | Purpose | Notes |
|-------|------|---------|-------|
| `id` | AutoField | Primary key | |
| `name` | CharField(255) | Display name | "Free Trial" |
| `slug` | SlugField(255) | URL identifier | Unique |
| `price` | DecimalField(10,2) | Monthly price | USD |
| `billing_cycle` | CharField(20) | Cycle type | monthly/annual |
| `annual_discount_percent` | DecimalField(5,2) | Annual discount | Default: 15.00, Range: 0-100 |
| `is_featured` | BooleanField | Highlight plan | Default: False |
| `features` | JSONField | Feature list | Array of strings |
| `is_active` | BooleanField | Available | Default: True |
| `is_internal` | BooleanField | Hidden plan | Default: False |
| `max_users` | IntegerField | User limit | Default: 1, Min: 1 |
| `max_sites` | IntegerField | Site limit | Default: 1, Min: 1 |
| `max_industries` | IntegerField | Sector limit | Nullable, Min: 1 |
| `max_author_profiles` | IntegerField | Author profiles | Default: 5, Min: 0 |
| `included_credits` | IntegerField | Monthly credits | Default: 0, Min: 0 |
| `extra_credit_price` | DecimalField(10,2) | Per-credit cost | Default: 0.01 |
| `allow_credit_topup` | BooleanField | Can buy credits | Default: True |
| `auto_credit_topup_threshold` | IntegerField | Auto-buy trigger | Nullable, Min: 0 |
| `auto_credit_topup_amount` | IntegerField | Auto-buy amount | Nullable, Min: 1 |
| `stripe_product_id` | CharField(255) | Stripe product | Nullable |
| `stripe_price_id` | CharField(255) | Stripe price | Nullable |
| `credits_per_month` | IntegerField | DEPRECATED | Use included_credits |
| `created_at` | DateTimeField | Creation time | Auto-set |

**Key Methods:**
- `get_effective_credits_per_month()` - Returns `included_credits` or `credits_per_month`
- `clean()` - Validates max_sites >= 1, included_credits >= 0

---

### User Model

**Location:** `backend/igny8_core/auth/models.py`  
**Table:** `igny8_users`  
**Inheritance:** `AbstractUser`

| Field | Type | Purpose | Notes |
|-------|------|---------|-------|
| `id` | AutoField | Primary key | |
| `email` | EmailField | Login email | Unique |
| `username` | CharField(150) | Username | Auto-generated from email |
| `password` | CharField(128) | Hashed password | Django auth |
| `first_name` | CharField(150) | First name | Optional |
| `last_name` | CharField(150) | Last name | Optional |
| `account` | FK(Account) | Tenant | Nullable, CASCADE, db_column='tenant_id' |
| `role` | CharField(20) | Permission level | developer/owner/admin/editor/viewer |
| `is_active` | BooleanField | Account enabled | Default: True |
| `is_staff` | BooleanField | Django admin | Default: False |
| `is_superuser` | BooleanField | All permissions | Default: False |
| `created_at` | DateTimeField | Join date | Auto-set |
| `updated_at` | DateTimeField | Last update | Auto-updated |

**USERNAME_FIELD:** `email`  
**REQUIRED_FIELDS:** `username`

**Key Methods:**
- `has_role(*roles)` - Check if user has any of specified roles
- `is_owner_or_admin()` - Returns `role in ['owner', 'admin']`
- `is_developer()` - Returns `role == 'developer'`

**Indexes:**
- `account` + `role`
- `email` (unique)

---

### Site Model

**Location:** `backend/igny8_core/auth/models.py`  
**Table:** `igny8_sites`  
**Inheritance:** `SoftDeletableModel`, `AccountBaseModel`

| Field | Type | Purpose | Notes |
|-------|------|---------|-------|
| `id` | AutoField | Primary key | |
| `account` | FK(Account) | Owner tenant | CASCADE, db_column='tenant_id', indexed |
| `name` | CharField(255) | Site name | "Tech Insights" |
| `slug` | SlugField(255) | URL identifier | Unique per account |
| `domain` | URLField | Site URL | Nullable, https:// enforced |
| `description` | TextField | Site purpose | Nullable |
| `industry` | FK(Industry) | Industry category | PROTECT, nullable |
| `is_active` | BooleanField | Enabled | Default: True, indexed |
| `status` | CharField(20) | Site state | active/inactive/suspended |
| `wp_url` | URLField | WordPress URL | LEGACY - use SiteIntegration |
| `wp_username` | CharField(255) | WP user | LEGACY |
| `wp_app_password` | CharField(255) | WP password | LEGACY |
| `wp_api_key` | CharField(255) | WP Bridge API key | LEGACY |
| `site_type` | CharField(50) | Site category | marketing/ecommerce/blog/portfolio/corporate |
| `hosting_type` | CharField(50) | Platform | igny8_sites/wordpress/shopify/multi |
| `seo_metadata` | JSONField | SEO data | Default: {} |
| `is_deleted` | BooleanField | Soft delete | Default: False, indexed |
| `deleted_at` | DateTimeField | Delete time | Nullable, indexed |
| `deleted_by` | FK(User) | Deleter | Nullable, SET_NULL |
| `delete_reason` | CharField(255) | Delete reason | Nullable |
| `restore_until` | DateTimeField | Restore deadline | Nullable, indexed |
| `created_at` | DateTimeField | Creation time | Auto-set |
| `updated_at` | DateTimeField | Last update | Auto-updated |

**Key Methods:**
- `get_active_sectors_count()` - Count active sectors
- `get_max_sectors_limit()` - Returns `account.plan.max_industries` or 5
- `can_add_sector()` - Check if under sector limit

**Unique Together:** `account` + `slug`

**Indexes:**
- `account` + `is_active`
- `account` + `status`
- `industry`
- `site_type`
- `hosting_type`

---

### Sector Model

**Location:** `backend/igny8_core/auth/models.py`  
**Table:** `igny8_sectors`  
**Inheritance:** `SoftDeletableModel`, `AccountBaseModel`

| Field | Type | Purpose | Notes |
|-------|------|---------|-------|
| `id` | AutoField | Primary key | |
| `account` | FK(Account) | Owner tenant | CASCADE, db_column='tenant_id', auto-set from site |
| `site` | FK(Site) | Parent site | CASCADE, indexed |
| `industry_sector` | FK(IndustrySector) | Sector template | PROTECT, nullable |
| `name` | CharField(255) | Sector name | "Web Development" |
| `slug` | SlugField(255) | URL identifier | Unique per site |
| `description` | TextField | Purpose | Nullable |
| `is_active` | BooleanField | Enabled | Default: True, indexed |
| `status` | CharField(20) | State | active/inactive |
| `is_deleted` | BooleanField | Soft delete | Default: False, indexed |
| `deleted_at` | DateTimeField | Delete time | Nullable, indexed |
| `deleted_by` | FK(User) | Deleter | Nullable, SET_NULL |
| `restore_until` | DateTimeField | Restore deadline | Nullable, indexed |
| `created_at` | DateTimeField | Creation time | Auto-set |
| `updated_at` | DateTimeField | Last update | Auto-updated |

**Key Properties:**
- `industry` (property) - Returns `industry_sector.industry` if set

**Key Methods:**
- `save()` - Auto-sets `account` from `site`, validates industry match, enforces sector limit

**Unique Together:** `site` + `slug`

**Indexes:**
- `site` + `is_active`
- `account` + `site`
- `industry_sector`

---

### Subscription Model (auth.models - LEGACY)

**Location:** `backend/igny8_core/auth/models.py`  
**Table:** `igny8_subscriptions`

| Field | Type | Purpose | Notes |
|-------|------|---------|-------|
| `id` | AutoField | Primary key | |
| `account` | OneToOneFK(Account) | Owner tenant | CASCADE, db_column='tenant_id' |
| `stripe_subscription_id` | CharField(255) | Stripe sub ID | Nullable, indexed |
| `payment_method` | CharField(30) | Payment type | stripe/paypal/bank_transfer |
| `external_payment_id` | CharField(255) | External ref | Nullable |
| `status` | CharField(20) | Sub state | active/past_due/canceled/trialing/pending_payment |
| `current_period_start` | DateTimeField | Billing start | Required |
| `current_period_end` | DateTimeField | Billing end | Required |
| `cancel_at_period_end` | BooleanField | Cancel scheduled | Default: False |
| `created_at` | DateTimeField | Creation time | Auto-set |
| `updated_at` | DateTimeField | Last update | Auto-updated |

**Indexes:**
- `status`

**⚠️ ISSUE:** This model exists in `auth` app but new billing flow references `billing.Subscription` which doesn't exist!

---

### Invoice Model

**Location:** `backend/igny8_core/business/billing/models.py`  
**Table:** `igny8_invoices`  
**Inheritance:** `AccountBaseModel`

| Field | Type | Purpose | Notes |
|-------|------|---------|-------|
| `id` | AutoField | Primary key | |
| `account` | FK(Account) | Owner tenant | CASCADE, db_column='tenant_id' |
| `invoice_number` | CharField(50) | Invoice ID | Unique, indexed |
| `subscription` | FK(Subscription) | Related sub | SET_NULL, nullable |
| `subtotal` | DecimalField(10,2) | Pre-tax amount | Default: 0 |
| `tax` | DecimalField(10,2) | Tax amount | Default: 0 |
| `total` | DecimalField(10,2) | Total amount | Default: 0 |
| `currency` | CharField(3) | Currency code | Default: USD |
| `status` | CharField(20) | Invoice state | draft/pending/paid/void/uncollectible |
| `invoice_date` | DateField | Issue date | Indexed |
| `due_date` | DateField | Payment due | Required |
| `paid_at` | DateTimeField | Payment time | Nullable |
| `line_items` | JSONField | Item details | Array of {description, amount, quantity} |
| `stripe_invoice_id` | CharField(255) | Stripe ref | Nullable |
| `payment_method` | CharField(50) | Payment type | Nullable |
| `billing_email` | EmailField | Contact email | Nullable |
| `billing_period_start` | DateTimeField | Period start | Nullable |
| `billing_period_end` | DateTimeField | Period end | Nullable |
| `notes` | TextField | Admin notes | Blank allowed |
| `metadata` | JSONField | Extra data | Default: {} |
| `created_at` | DateTimeField | Creation time | Auto-set |
| `updated_at` | DateTimeField | Last update | Auto-updated |

**Key Methods:**
- `add_line_item(description, quantity, unit_price, amount)` - Append item to JSON
- `calculate_totals()` - Recompute subtotal/total from line_items
- Properties: `subtotal_amount`, `tax_amount`, `total_amount` (legacy aliases)

**Indexes:**
- `account` + `status`
- `account` + `invoice_date`
- `invoice_number` (unique)

**⚠️ ISSUE:** References `Subscription` FK but points to `auth.Subscription`, not `billing.Subscription`!

---

### CreditTransaction Model

**Location:** `backend/igny8_core/business/billing/models.py`  
**Table:** `igny8_credit_transactions`  
**Inheritance:** `AccountBaseModel`

| Field | Type | Purpose | Notes |
|-------|------|---------|-------|
| `id` | AutoField | Primary key | |
| `account` | FK(Account) | Owner tenant | CASCADE, db_column='tenant_id' |
| `transaction_type` | CharField(20) | Transaction category | purchase/subscription/refund/deduction/adjustment |
| `amount` | IntegerField | Credit change | Positive=add, Negative=subtract |
| `balance_after` | IntegerField | New balance | After transaction |
| `description` | CharField(255) | Human description | "Free plan credits" |
| `metadata` | JSONField | Extra context | Default: {} |
| `reference_id` | CharField(255) | External ref | Blank allowed |
| `created_at` | DateTimeField | Transaction time | Auto-set |

**Indexes:**
- `account` + `transaction_type`
- `account` + `created_at`

---

## Architectural Gaps & Issues

### 🔴 CRITICAL: Duplicate Subscription Models

**Problem:**
- `Subscription` model exists in **TWO locations**:
  1. `auth.models.Subscription` - LEGACY model (currently in use)
  2. `billing.Subscription` - Referenced in code but **DOESN'T EXIST**

**Evidence:**
```python
# In auth/serializers.py RegisterSerializer.create()
from igny8_core.business.billing.models import Subscription  # ❌ DOESN'T EXIST

subscription = Subscription.objects.create(...)  # FAILS
```

**Current State:**
- Database table: `igny8_subscriptions` (from `auth.Subscription`)
- Code references: Mix of `auth.Subscription` and `billing.Subscription`
- Invoice FK: Points to `igny8_core_auth.Subscription`

**Impact:**
- Registration fails for paid plans
- Invoice creation broken
- Subscription queries inconsistent

**Solution:**
1. **Option A (Recommended):** Migrate to single `billing.Subscription` model
   - Create `billing/models.py::Subscription` with all fields from `auth.Subscription`
   - Add migration to point Invoice FK to new model
   - Update all imports to use `billing.Subscription`
   - Deprecate `auth.Subscription`

2. **Option B:** Stick with `auth.Subscription`
   - Update all imports in billing code to use `auth.Subscription`
   - Remove references to non-existent `billing.Subscription`

---

### 🔴 CRITICAL: Account.owner Circular Dependency

**Problem:**
- `Account.owner` → FK(User)
- `User.account` → FK(Account)
- **Chicken-and-egg during registration**

**Current Workaround:**
```python
# Step 1: Create User WITHOUT account
user = User.objects.create_user(account=None)

# Step 2: Create Account WITH user as owner
account = Account.objects.create(owner=user)

# Step 3: Update User to link back to Account
user.account = account
user.save()
```

**Issues:**
- User exists briefly without account (breaks middleware assumptions)
- Three database writes for one logical operation
- Race condition if another request hits between steps 1-3

**Solution:**
- Make `Account.owner` nullable with `SET_NULL` on delete (already done ✓)
- OR: Remove `Account.owner` field entirely, derive from `User.objects.filter(account=account, role='owner').first()`

---

### 🟡 MEDIUM: Duplicate tenant_id Column Name

**Problem:**
- All `AccountBaseModel` children use `db_column='tenant_id'`
- Field name in Django: `account`
- Field name in DB: `tenant_id`
- **Confusing when writing raw SQL or debugging**

**Evidence:**
```python
class AccountBaseModel(models.Model):
    account = models.ForeignKey(
        'igny8_core_auth.Account',
        db_column='tenant_id'  # ← DB column name
    )
```

**Impact:**
- `SELECT * FROM igny8_sites WHERE account=1` ❌ (SQL fails)
- Must use: `SELECT * FROM igny8_sites WHERE tenant_id=1` ✓
- ORM queries work fine but raw SQL doesn't

**Solution:**
- **Option A:** Rename DB column to `account_id` (requires migration, breaks existing data)
- **Option B:** Keep as-is, document clearly (current approach)

---

### 🟡 MEDIUM: Sector Limit Inconsistency

**Problem:**
- Plan has `max_industries` field for sector limits
- Field name implies "number of industries" not "number of sectors per site"
- Default sector limit hardcoded as 5 in `Site.get_max_sectors_limit()`

**Evidence:**
```python
def get_max_sectors_limit(self):
    if self.account.plan.max_industries is not None:
        return self.account.plan.max_industries
    return 5  # Hardcoded fallback
```

**Impact:**
- Field name misleading (sectors ≠ industries)
- Free plan has `max_industries=NULL` → defaults to 5 sectors
- No way to configure "unlimited sectors"

**Solution:**
- Rename `Plan.max_industries` → `Plan.max_sectors_per_site` (migration required)
- Use `max_sectors_per_site=0` to mean unlimited
- Update validation logic to handle 0 = unlimited

---

### 🟡 MEDIUM: Billing Fields Duplication

**Problem:**
- Billing address fields exist in **TWO places**:
  1. `Account` model (billing_email, billing_address_line1, etc.)
  2. `Invoice` model (billing_email)

**Current State:**
```python
# Account model
billing_email = models.EmailField(...)
billing_address_line1 = models.CharField(...)
billing_city = models.CharField(...)
# ... 7 billing fields

# Invoice model
billing_email = models.EmailField(...)  # DUPLICATE
```

**Impact:**
- Data can become inconsistent
- Which is source of truth?
- Invoice should snapshot billing info at time of invoice creation

**Solution:**
- Keep billing fields on `Account` as primary source
- Invoice snapshots should store full billing data in `metadata` JSON:
  ```json
  {
    "billing_snapshot": {
      "email": "john@example.com",
      "address": {...}
    }
  }
  ```
- Remove `Invoice.billing_email` field

---

### 🟡 MEDIUM: No Subscription Model in billing App

**Problem:**
- `billing` app has Invoice, Payment, CreditTransaction models
- Missing central `Subscription` model (exists in `auth` instead)
- Subscription should be in billing domain

**Impact:**
- Billing logic split across two apps
- `Invoice.subscription` FK points to different app
- Harder to maintain billing as cohesive module

**Solution:**
- Create `billing.models.Subscription` with fields:
  - `account` (OneToOne)
  - `plan` (FK to auth.Plan)
  - `status` (active/past_due/canceled)
  - `current_period_start/end`
  - `payment_method`
  - `stripe_subscription_id`
- Migrate data from `auth.Subscription` → `billing.Subscription`
- Update Invoice FK

---

### 🟡 MEDIUM: Site Industry Should Not Be Nullable

**Problem:**
- `Site.industry` is nullable but required for sector creation
- Sectors validate `industry_sector.industry == site.industry`
- **If site.industry is NULL, sector creation always fails**

**Evidence:**
```python
# In Sector.save()
if self.industry_sector.industry != self.site.industry:
    raise ValidationError("Sector must belong to site's industry")
```

**Impact:**
- Sites created without industry can never have sectors
- Confusing UX: "Why can't I add sectors?"

**Solution:**
- Make `Site.industry` required (remove `null=True, blank=True`)
- Require industry selection during site creation
- Migration: Set default industry for existing sites with NULL

---

### 🟡 MEDIUM: Credits Auto-Update Missing

**Problem:**
- `Account.credits` is manually updated
- No automatic credit refresh on subscription renewal
- No automatic credit deduction tracking

**Current State:**
```python
# Manual credit update
account.credits += 1000
account.save()

# CreditTransaction created separately
CreditTransaction.objects.create(
    account=account,
    amount=1000,
    balance_after=account.credits
)
```

**Impact:**
- Easy to forget credit transaction logging
- Balance can become inconsistent
- No single source of truth

**Solution:**
- Create `CreditService` with methods:
  - `add_credits(account, amount, description, metadata)`
  - `deduct_credits(account, amount, description, metadata)`
- Service handles BOTH account update AND transaction logging atomically
- All credit changes must go through service

---

### 🟢 LOW: Legacy WordPress Fields on Site

**Problem:**
- Site model has 4 WordPress-specific fields:
  - `wp_url`, `wp_username`, `wp_app_password`, `wp_api_key`
- New `SiteIntegration` model exists for integrations
- **Duplication of WordPress config**

**Impact:**
- Two places to store same data
- Code uses legacy fields, SiteIntegration ignored
- Can't support multiple integrations per site

**Solution:**
- Mark legacy WP fields as deprecated
- Migrate existing WP configs to `SiteIntegration` records
- Update code to use `SiteIntegration` model
- Remove legacy fields in future migration

---

### 🟢 LOW: Plan.credits_per_month Deprecated Field

**Problem:**
- Plan has both `credits_per_month` (old) and `included_credits` (new)
- `get_effective_credits_per_month()` checks both
- **Confusing which field to use**

**Current Code:**
```python
def get_effective_credits_per_month(self):
    return self.included_credits if self.included_credits > 0 else self.credits_per_month
```

**Impact:**
- Admin UI shows both fields
- Risk of setting wrong field
- Database stores redundant data

**Solution:**
- Data migration: Copy `credits_per_month` → `included_credits` where needed
- Remove `credits_per_month` field
- Update method to just return `included_credits`

---

### 🟢 LOW: SiteUserAccess Not Used

**Problem:**
- `SiteUserAccess` model exists for granular site permissions
- **Never created during registration or site setup**
- Code assumes owners/admins have access to all sites

**Current State:**
```python
# Registration creates:
- User (role='owner')
- Account
- Site

# MISSING: SiteUserAccess record linking User ↔ Site
```

**Impact:**
- Granular permissions not enforced
- Role-based access only (no site-level restrictions)
- Model exists but unused

**Solution:**
- Create `SiteUserAccess` during site creation for owner
- Create during user invitation for specific sites
- Update ViewSets to filter by `SiteUserAccess` for non-owner roles

---

### 🟢 LOW: No Automatic Slug Generation

**Problem:**
- Slugs manually generated from names with counters
- Logic duplicated in multiple serializers
- No utility function

**Evidence:**
```python
# In RegisterSerializer
base_slug = account_name.lower().replace(' ', '-')[:50]
slug = base_slug
counter = 1
while Account.objects.filter(slug=slug).exists():
    slug = f"{base_slug}-{counter}"
    counter += 1
```

**Impact:**
- Duplicate code in Account, Site, Sector serializers
- Inconsistent slug generation logic
- Hard to maintain

**Solution:**
- Create utility: `generate_unique_slug(model, name_field, base_name)`
- Use in all serializers
- Handle edge cases (special chars, max length) consistently

---

## Summary of Issues

### By Severity

**🔴 CRITICAL (Fix Immediately):**
1. Duplicate Subscription models - registration broken
2. Account.owner circular dependency - race conditions

**🟡 MEDIUM (Fix Soon):**
3. tenant_id vs account field naming confusion
4. Sector limit field misnamed (max_industries)
5. Billing fields duplicated (Account + Invoice)
6. No Subscription in billing app
7. Site.industry should be required
8. No automatic credit updates

**🟢 LOW (Technical Debt):**
9. Legacy WordPress fields unused
10. Plan.credits_per_month deprecated
11. SiteUserAccess never created
12. No slug generation utility

---

## Recommendations

### Immediate Actions (Critical Fixes)

#### 1. Fix Subscription Model Duplication

**Steps:**
```python
# backend/igny8_core/business/billing/models.py
class Subscription(AccountBaseModel):
    """Account subscription - moved from auth app"""
    plan = models.ForeignKey('igny8_core_auth.Plan', on_delete=models.PROTECT)
    status = models.CharField(max_length=20, choices=STATUS_CHOICES)
    payment_method = models.CharField(max_length=30)
    stripe_subscription_id = models.CharField(max_length=255, null=True, blank=True)
    external_payment_id = models.CharField(max_length=255, null=True, blank=True)
    current_period_start = models.DateTimeField()
    current_period_end = models.DateTimeField()
    cancel_at_period_end = models.BooleanField(default=False)
    
    class Meta:
        app_label = 'billing'
        db_table = 'igny8_subscriptions'  # Reuse existing table
```

**Migration:**
```python
# Create migration to change Invoice.subscription FK
# Point to billing.Subscription instead of auth.Subscription
# No data migration needed - table name stays same
```

**Update Imports:**
```python
# Update everywhere:
from igny8_core.business.billing.models import Subscription
# Instead of:
from igny8_core.auth.models import Subscription
```

---

#### 2. Resolve Account.owner Circular Dependency

**Recommended Approach:**
Make `Account.owner` derivable from User role instead of storing FK.

**Changes:**
```python
# Account model
class Account(SoftDeletableModel):
    # REMOVE: owner = models.ForeignKey(...)
    
    @property
    def owner(self):
        """Get account owner from users"""
        return self.users.filter(role='owner').first()
    
    def set_owner(self, user):
        """Make user the account owner"""
        # Remove owner role from previous owner
        self.users.filter(role='owner').update(role='admin')
        # Set new owner
        user.role = 'owner'
        user.save()
```

**Registration Flow (Simplified):**
```python
# Single transaction, no circular dependency
with transaction.atomic():
    account = Account.objects.create(
        name=account_name,
        slug=slug,
        plan=plan,
        credits=initial_credits
    )
    
    user = User.objects.create_user(
        account=account,
        role='owner',  # This makes them the owner
        ...
    )
```

---

### Short-Term Improvements (Medium Priority)

#### 3. Rename Plan.max_industries → max_sectors_per_site

**Migration:**
```python
migrations.RenameField(
    model_name='plan',
    old_name='max_industries',
    new_name='max_sectors_per_site',
)
```

**Update Validation:**
```python
def get_max_sectors_limit(self):
    limit = self.account.plan.max_sectors_per_site
    if limit is None or limit == 0:
        return float('inf')  # Unlimited
    return limit
```

---

#### 4. Make Site.industry Required

**Migration:**
```python
# Set default industry for existing sites
migrations.RunPython(set_default_industry)

# Make field required
migrations.AlterField(
    model_name='site',
    name='industry',
    field=models.ForeignKey(
        'igny8_core_auth.Industry',
        on_delete=models.PROTECT,
        related_name='sites'
        # Remove: null=True, blank=True
    ),
)
```

---

#### 5. Create CreditService for Automatic Updates

**Implementation:**
```python
# backend/igny8_core/business/billing/services/credit_service.py

class CreditService:
    @staticmethod
    @transaction.atomic
    def add_credits(account, amount, description, metadata=None):
        """Add credits and log transaction atomically"""
        # Update account
        account.credits += amount
        account.save()
        
        # Log transaction
        CreditTransaction.objects.create(
            account=account,
            transaction_type='purchase',
            amount=amount,
            balance_after=account.credits,
            description=description,
            metadata=metadata or {}
        )
        
        return account.credits
    
    @staticmethod
    @transaction.atomic
    def deduct_credits(account, amount, description, metadata=None):
        """Deduct credits and log transaction atomically"""
        if account.credits < amount:
            raise InsufficientCreditsError(
                f"Need {amount} credits, have {account.credits}"
            )
        
        account.credits -= amount
        account.save()
        
        CreditTransaction.objects.create(
            account=account,
            transaction_type='deduction',
            amount=-amount,
            balance_after=account.credits,
            description=description,
            metadata=metadata or {}
        )
        
        return account.credits
```

**Usage:**
```python
# Instead of:
account.credits += 1000
account.save()
CreditTransaction.objects.create(...)

# Use:
CreditService.add_credits(
    account=account,
    amount=1000,
    description='Monthly subscription credits',
    metadata={'plan_slug': 'starter'}
)
```

---

### Long-Term Refactoring (Technical Debt)

#### 6. Create Slug Generation Utility

**Implementation:**
```python
# backend/igny8_core/common/utils.py

def generate_unique_slug(model_class, base_name, filters=None, max_length=50):
    """
    Generate unique slug from base name
    
    Args:
        model_class: Django model to check uniqueness against
        base_name: Source string for slug
        filters: Additional Q filters for uniqueness check
        max_length: Maximum slug length
    
    Returns:
        Unique slug string
    """
    import re
    from django.utils.text import slugify
    
    # Clean and slugify
    base_slug = slugify(base_name)[:max_length]
    
    # Remove special characters
    base_slug = re.sub(r'[^a-z0-9-]', '', base_slug)
    
    # Check uniqueness
    slug = base_slug
    counter = 1
    
    while True:
        query = model_class.objects.filter(slug=slug)
        if filters:
            query = query.filter(filters)
        
        if not query.exists():
            return slug
        
        # Append counter
        suffix = f"-{counter}"
        available_length = max_length - len(suffix)
        slug = f"{base_slug[:available_length]}{suffix}"
        counter += 1
```

**Usage:**
```python
# In serializers
from igny8_core.common.utils import generate_unique_slug

slug = generate_unique_slug(
    model_class=Account,
    base_name=account_name,
    max_length=50
)
```

---

#### 7. Migrate WordPress Fields to SiteIntegration

**Step 1: Data Migration**
```python
def migrate_wp_fields(apps, schema_editor):
    Site = apps.get_model('igny8_core_auth', 'Site')
    SiteIntegration = apps.get_model('igny8_core_business_integration', 'SiteIntegration')
    
    for site in Site.objects.exclude(wp_url__isnull=True):
        SiteIntegration.objects.create(
            account=site.account,
            site=site,
            platform='wordpress',
            api_url=site.wp_url,
            credentials={
                'username': site.wp_username,
                'app_password': site.wp_app_password,
                'api_key': site.wp_api_key
            },
            is_active=True
        )
```

**Step 2: Remove Legacy Fields**
```python
migrations.RemoveField(model_name='site', name='wp_url')
migrations.RemoveField(model_name='site', name='wp_username')
migrations.RemoveField(model_name='site', name='wp_app_password')
migrations.RemoveField(model_name='site', name='wp_api_key')
```

---

#### 8. Auto-Create SiteUserAccess Records

**Update Site Creation:**
```python
# In SiteViewSet.perform_create()
def perform_create(self, serializer):
    site = serializer.save(account=self.request.account)
    
    # Auto-grant access to owner
    if self.request.user.role in ['owner', 'admin']:
        SiteUserAccess.objects.create(
            user=self.request.user,
            site=site,
            granted_by=self.request.user
        )
```

**Update Registration:**
```python
# After site creation in RegisterSerializer
if default_site_created:
    SiteUserAccess.objects.create(
        user=user,
        site=default_site,
        granted_by=user
    )
```

---

### Database Optimization

#### Add Missing Indexes

```python
# Account model
class Meta:
    indexes = [
        models.Index(fields=['slug']),  # Already exists
        models.Index(fields=['status']),  # Already exists
        models.Index(fields=['plan', 'status']),  # NEW - for plan filtering
        models.Index(fields=['created_at']),  # NEW - for sorting
    ]

# Site model
class Meta:
    indexes = [
        # Existing indexes...
        models.Index(fields=['account', 'industry']),  # NEW - for filtering
        models.Index(fields=['created_at']),  # NEW - for sorting
    ]

# Sector model
class Meta:
    indexes = [
        # Existing indexes...
        models.Index(fields=['created_at']),  # NEW - for sorting
    ]
```

---

### API Consistency

#### Standardize Response Formats

**Current Issues:**
- Some endpoints return `{data: {...}}`, others return `{...}` directly
- Error formats inconsistent
- Pagination formats vary

**Recommendations:**
```python
# Standard success response
{
    "success": true,
    "data": {...},
    "message": "Operation completed successfully"
}

# Standard error response
{
    "success": false,
    "error": {
        "code": "VALIDATION_ERROR",
        "message": "Site limit reached",
        "details": {...}
    }
}

# Standard pagination
{
    "success": true,
    "data": [...],
    "pagination": {
        "count": 100,
        "page": 1,
        "pages": 10,
        "page_size": 10
    }
}
```

---

## Conclusion

### Current State Summary

**✅ Working Well:**
- Account isolation via `AccountBaseModel`
- Plan-based limits enforcement
- Credit system with transaction logging
- Soft delete functionality
- Multi-level hierarchy (Account → Site → Sector)

**⚠️ Needs Attention:**
- Subscription model duplication (breaks paid signups)
- Circular dependency in Account/User relationship
- Field naming inconsistencies (tenant_id, max_industries)
- Missing automatic credit management
- Unused models (SiteUserAccess)

**📈 Future Improvements:**
- Centralized credit service
- Unified billing domain
- Better slug generation
- Migration to SiteIntegration for all platforms
- Comprehensive API response standardization

### Migration Priority

**Phase 1 (Week 1):** Fix critical issues
- Consolidate Subscription model
- Fix Account.owner circular dependency

**Phase 2 (Week 2-3):** Medium priority improvements
- Rename fields for clarity
- Make Site.industry required
- Implement CreditService

**Phase 3 (Week 4+):** Technical debt cleanup
- Migrate WordPress fields
- Create slug utility
- Implement SiteUserAccess properly
- Add missing indexes

---

**Document Version:** 1.0  
**Last Updated:** December 8, 2025  
**Maintainer:** Development Team

