From 73d7a6953be71497863330138dbb24bdca143e6e Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Mon, 8 Dec 2025 15:51:36 +0000 Subject: [PATCH] tenaancy master doc --- TENANCY-WORKFLOW-DOCUMENTATION.md | 1631 +++++++++++++++++++++++++++++ 1 file changed, 1631 insertions(+) create mode 100644 TENANCY-WORKFLOW-DOCUMENTATION.md diff --git a/TENANCY-WORKFLOW-DOCUMENTATION.md b/TENANCY-WORKFLOW-DOCUMENTATION.md new file mode 100644 index 00000000..e63c4c5e --- /dev/null +++ b/TENANCY-WORKFLOW-DOCUMENTATION.md @@ -0,0 +1,1631 @@ +# 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 +