Files
igny8/multi-tenancy/CRITICAL-GAPS-SIGNUP-TO-SITE-WORKFLOW.md
IGNY8 VPS (Salman) c54db6c2d9 reorg
2025-12-08 20:15:09 +00:00

17 KiB

CRITICAL GAPS: Signup to Site Creation Workflow

Analysis Date: December 8, 2025
Status: 🔴 BLOCKING ISSUES FOUND


Executive Summary

CRITICAL FINDING: The registration flow for paid plans is COMPLETELY BROKEN due to missing model definition and multiple architectural inconsistencies. Free trial signups work but have significant gaps.

Impact:

  • Paid plan signups (starter/growth/scale) FAIL on registration
  • ⚠️ Free trial signups work but create incomplete data structures
  • ⚠️ Site creation has validation issues and missing relationships
  • ⚠️ Duplicate fields cause data inconsistency risks

🔴 CRITICAL ISSUES (Must Fix Immediately)

1. MISSING MODEL: billing.Subscription Does Not Exist

Problem:
RegisterSerializer.create() imports and tries to use billing.Subscription which DOES NOT EXIST:

# In auth/serializers.py line 291
from igny8_core.business.billing.models import Subscription  # ❌ IMPORT FAILS

Evidence:

# Python shell test:
>>> from igny8_core.business.billing.models import Subscription
ImportError: cannot import name 'Subscription' from 'igny8_core.business.billing.models'

What Actually Exists:

  • auth.Subscription model at igny8_core/auth/models.py line 218
  • Database table: igny8_subscriptions (created by auth.Subscription)

Impact:

  • Registration with paid plans (starter, growth, scale) FAILS IMMEDIATELY
  • Line 403-412 in RegisterSerializer.create() crashes on paid signups:
    subscription = Subscription.objects.create(...)  # ❌ CRASHES
    

Root Cause: Documentation and code assume billing.Subscription was created but it was never implemented.

Fix Required:

  1. Option A (Recommended): Use existing auth.Subscription

    # Change line 291 in auth/serializers.py
    from igny8_core.auth.models import Subscription
    
  2. Option B: Create billing.Subscription and migrate

    • Create model in billing/models.py
    • Create migration to point Invoice FK to new model
    • Data migration to copy existing records
    • Update all imports

2. MISSING FIELD: Subscription.plan Does Not Exist

Problem:
The Subscription model in auth/models.py has NO plan field, but InvoiceService and documentation assume it exists.

Evidence:

# Database inspection shows:
class Igny8Subscriptions(models.Model):
    tenant = models.OneToOneField('Igny8Tenants')
    stripe_subscription_id = CharField
    payment_method = CharField
    external_payment_id = CharField
    status = CharField
    current_period_start = DateTimeField
    current_period_end = DateTimeField
    cancel_at_period_end = BooleanField
    # ❌ NO PLAN FIELD

Code Expects:

# In InvoiceService.create_subscription_invoice()
subscription.plan  # ❌ AttributeError

Impact:

  • Invoice creation for subscriptions WILL FAIL
  • Cannot determine which plan a subscription belongs to
  • Credit allocation logic broken

Fix Required: Add plan field to Subscription model:

class Subscription(models.Model):
    # ... existing fields ...
    plan = models.ForeignKey('igny8_core_auth.Plan', on_delete=models.PROTECT, related_name='subscriptions')

Migration required to add column and populate from Account.plan.


3. Account.owner Circular Dependency Race Condition

Problem:
Registration creates User and Account in 3 separate steps, causing temporary invalid state:

# Step 1: User created WITHOUT account
user = User.objects.create_user(account=None)  # ⚠️ User has no tenant

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

# Step 3: User updated to link to account
user.account = account
user.save()  # ⚠️ Three DB writes for one logical operation

Impact:

  • Between steps 1-3, user exists without account (tenant isolation broken)
  • If step 2 or 3 fails, orphaned user exists
  • Race condition: if another request hits during steps 1-3, middleware fails
  • Triple database writes for single logical operation

Fix Required: Use single transaction or remove Account.owner FK entirely and derive from role:

# Option 1: Single transaction (already wrapped, but still 3 writes)
# Option 2: Remove Account.owner and use property
@property
def owner(self):
    return self.users.filter(role='owner').first()

4. Site.industry Should Be Required But Is Nullable

Problem:
Site.industry is null=True, blank=True but sector creation REQUIRES site to have industry:

# In Sector.save() line 541
if self.industry_sector.industry != self.site.industry:
    raise ValidationError("Sector must belong to site's industry")

Impact:

  • Sites can be created without industry
  • When user tries to add sector: VALIDATION FAILS (industry is None)
  • No sectors can ever be added to sites without industry
  • Confusing UX: "Why can't I add sectors?"

Evidence:

# Database schema:
industry = models.ForeignKey(Igny8Industries, blank=True, null=True)  # ❌ NULLABLE

Fix Required:

  1. Make field required: Site.industrynull=False, blank=False
  2. Migration: Set default industry for existing NULL sites
  3. Update serializers to require industry during site creation

5. Free Plan Credits Mismatch

Problem:
Documentation says free plan gives 1000 credits, but actual database has only 100:

Documentation:

Free Trial | free | 0.00 | 1000 credits | 1 site | 1 user

Actual Database:

Free Plan | free | 0.00 | 100 credits | 1 site | 1 user

Impact:

  • New users get 10x fewer credits than documented
  • Sales/marketing materials may be wrong
  • User expectations mismatched

Fix Required: Update Plan record or documentation to match:

UPDATE igny8_plans SET included_credits = 1000 WHERE slug = 'free';

🟡 MEDIUM PRIORITY ISSUES (Fix Soon)

6. Duplicate Billing Email Fields

Problem:
billing_email exists in TWO models:

  1. Account.billing_email - Primary billing contact
  2. Invoice.billing_email - Email on invoice (snapshot)

Current Code:

# Account model line 106
billing_email = models.EmailField(blank=True, null=True)

# Invoice model line 213  
billing_email = models.EmailField(null=True, blank=True)

Impact:

  • Which is source of truth?
  • Data can become inconsistent
  • Invoice should snapshot billing info at creation time

Fix Required:

  • Keep Account.billing_email as primary
  • Invoice should copy from Account at creation time
  • OR: Store full billing snapshot in Invoice.metadata:
    {
      "billing_snapshot": {
        "email": "john@example.com",
        "address_line1": "123 Main St",
        ...
      }
    }
    

7. Plan.max_industries Misnamed Field

Problem:
Field is called max_industries but controls sectors per site, not industries:

# Plan model line 177
max_industries = models.IntegerField(help_text="Optional limit for industries/sectors")

# Site model line 371
def get_max_sectors_limit(self):
    return self.account.plan.max_industries  # ❌ Confusing name

Evidence from Database:

Free Plan: max_industries = 1  (means 1 sector per site, NOT 1 industry)

Impact:

  • Misleading field name (developers confused)
  • Documentation uses "max_industries" and "max_sectors_per_site" inconsistently
  • No way to configure unlimited sectors (NULL means fallback to 5)

Fix Required:

  1. Rename field: max_industriesmax_sectors_per_site
  2. Migration to rename column
  3. Update all references in code
  4. Use 0 to mean unlimited

8. Duplicate Subscription Payment Method

Problem:
Payment method stored in THREE places:

  1. Account.payment_method - Account default
  2. Subscription.payment_method - Subscription payment method
  3. AccountPaymentMethod.type - Saved payment methods

Current State:

# Account line 87
payment_method = models.CharField(default='stripe')

# Subscription line 231
payment_method = models.CharField(default='stripe')  

# AccountPaymentMethod line 476
type = models.CharField(PAYMENT_METHOD_CHOICES)

Impact:

  • Three fields that can be out of sync
  • Which one is used for billing?
  • AccountPaymentMethod is proper design, others redundant

Fix Required:

  • Use AccountPaymentMethod as single source of truth
  • Deprecate Account.payment_method and Subscription.payment_method
  • Add migration to create AccountPaymentMethod records from existing data

9. tenant_id vs account Field Name Confusion

Problem:
Django field name is account, database column name is tenant_id:

class AccountBaseModel(models.Model):
    account = models.ForeignKey(db_column='tenant_id')  # ❌ Confusing

Impact:

  • ORM uses account, raw SQL uses tenant_id
  • Debugging confusion: "Why isn't account=1 working in SQL?"
  • Must use: SELECT * FROM igny8_sites WHERE tenant_id=1 (not account=1)

Fix Required:

  • Option A: Rename column to account_id (requires migration, data untouched)
  • Option B: Keep as-is, document clearly (current approach)

Recommend Option A for consistency.


10. SiteUserAccess Never Created

Problem:
SiteUserAccess model exists for granular site permissions but is NEVER CREATED:

Expected Flow:

# During site creation
SiteUserAccess.objects.create(user=owner, site=site)

Actual Flow:

# Site created, NO SiteUserAccess record
site = Site.objects.create(...)  # ✓ Site exists
# ❌ No SiteUserAccess created

Impact:

  • Granular site permissions not enforced
  • Model exists but unused (dead code)
  • User.get_accessible_sites() checks SiteUserAccess but it's always empty
  • Only role-based access works (owner/admin see all)

Fix Required: Auto-create SiteUserAccess on site creation:

# In SiteViewSet.perform_create()
site = serializer.save()
if self.request.user.role in ['owner', 'admin']:
    SiteUserAccess.objects.create(
        user=self.request.user,
        site=site,
        granted_by=self.request.user
    )

11. Credits Auto-Update Missing

Problem:
Account credits manually updated, no service layer:

# Current approach (scattered throughout codebase)
account.credits += 1000
account.save()

# Separate transaction log (can be forgotten)
CreditTransaction.objects.create(...)

Impact:

  • Easy to forget logging transaction
  • Balance can become inconsistent
  • No atomic updates
  • No single source of truth

Fix Required: Create CreditService for atomic operations:

class CreditService:
    @staticmethod
    @transaction.atomic
    def add_credits(account, amount, description, metadata=None):
        account.credits += amount
        account.save()
        
        CreditTransaction.objects.create(
            account=account,
            transaction_type='purchase',
            amount=amount,
            balance_after=account.credits,
            description=description,
            metadata=metadata or {}
        )
        return account.credits

🟢 LOW PRIORITY (Technical Debt)

12. Legacy WordPress Fields Unused

Problem:
Site model has 4 WordPress fields but new SiteIntegration model exists:

# Site model (legacy)
wp_url = models.URLField(help_text="legacy - use SiteIntegration")
wp_username = CharField
wp_app_password = CharField  
wp_api_key = CharField

# Newer approach
SiteIntegration.platform = 'wordpress'
SiteIntegration.credentials = {...}

Fix Required:

  • Mark fields as deprecated
  • Migrate to SiteIntegration
  • Remove legacy fields in future release

13. Plan.credits_per_month Deprecated

Problem:
Two fields for same purpose:

credits_per_month = IntegerField(default=0)  # ❌ Deprecated
included_credits = IntegerField(default=0)   # ✓ Use this

def get_effective_credits_per_month(self):
    return self.included_credits if self.included_credits > 0 else self.credits_per_month

Fix Required:

  • Data migration: Copy to included_credits
  • Remove credits_per_month field
  • Update method to just return included_credits

14. No Slug Generation Utility

Problem:
Slug generation duplicated in Account, Site, Sector serializers:

# Duplicated 3+ times:
base_slug = name.lower().replace(' ', '-')[:50]
slug = base_slug
counter = 1
while Model.objects.filter(slug=slug).exists():
    slug = f"{base_slug}-{counter}"
    counter += 1

Fix Required: Create utility function:

def generate_unique_slug(model_class, base_name, filters=None, max_length=50):
    ...

📊 Summary by Severity

🔴 CRITICAL (Blocking - Fix Now)

  1. billing.Subscription does not exist - Paid signups FAIL
  2. Subscription.plan field missing - Invoice creation broken
  3. ⚠️ Account.owner circular dependency - Race condition risk
  4. ⚠️ Site.industry is nullable - Sector creation fails
  5. ⚠️ Free plan credits mismatch - 100 vs 1000 credits

IMMEDIATE ACTION REQUIRED:

# Fix #1: Update import in auth/serializers.py line 291
from igny8_core.auth.models import Subscription

# Fix #2: Add migration for Subscription.plan field
# Fix #4: Make Site.industry required
# Fix #5: Update Plan.included_credits to 1000

🟡 MEDIUM (Fix This Sprint)

  1. Duplicate billing_email fields
  2. Plan.max_industries misnamed
  3. Duplicate payment_method fields (3 places)
  4. tenant_id vs account naming confusion
  5. SiteUserAccess never created
  6. No CreditService for atomic updates

🟢 LOW (Technical Debt)

  1. Legacy WordPress fields
  2. Plan.credits_per_month deprecated
  3. No slug generation utility

🔧 Required Actions Before Production

Phase 1: Emergency Fixes (Today)

# 1. Fix Subscription import
# File: backend/igny8_core/auth/serializers.py line 291
from igny8_core.auth.models import Subscription  # Changed from billing.models

# 2. Add Subscription.plan field
# New migration:
class Migration:
    operations = [
        migrations.AddField(
            model_name='subscription',
            name='plan',
            field=models.ForeignKey(
                'igny8_core_auth.Plan',
                on_delete=models.PROTECT,
                related_name='subscriptions',
                null=True  # Temporarily nullable for data migration
            ),
        ),
        # Data migration: Copy plan from account
        migrations.RunPython(copy_plan_from_account),
        # Make non-nullable
        migrations.AlterField(
            model_name='subscription',
            name='plan',
            field=models.ForeignKey(
                'igny8_core_auth.Plan',
                on_delete=models.PROTECT,
                related_name='subscriptions'
            ),
        ),
    ]

Phase 2: Data Integrity (This Week)

-- Fix free plan credits
UPDATE igny8_plans SET included_credits = 1000 WHERE slug = 'free';

-- Make Site.industry required (after setting defaults)
UPDATE igny8_sites SET industry_id = 2 WHERE industry_id IS NULL;  -- Technology
ALTER TABLE igny8_sites ALTER COLUMN industry_id SET NOT NULL;

Phase 3: Architecture Improvements (Next Sprint)

  1. Create CreditService
  2. Auto-create SiteUserAccess
  3. Rename max_industries → max_sectors_per_site
  4. Consolidate payment method fields

🧪 Testing Required After Fixes

Test 1: Free Trial Signup

POST /api/v1/auth/register/
{
  "email": "test@example.com",
  "password": "Test123!",
  "password_confirm": "Test123!",
  "account_name": "Test Account",
  "plan_slug": "free"
}

# Expected:
# ✓ User created
# ✓ Account created with 1000 credits (not 100)
# ✓ CreditTransaction logged
# ✓ No errors

Test 2: Paid Plan Signup

POST /api/v1/auth/register/
{
  "email": "paid@example.com",
  "password": "Test123!",
  "password_confirm": "Test123!",
  "account_name": "Paid Account",
  "plan_slug": "starter",
  "payment_method": "bank_transfer"
}

# Expected:
# ✓ User created
# ✓ Account created with status='pending_payment'
# ✓ Subscription created with plan FK
# ✓ Invoice created
# ✓ AccountPaymentMethod created
# ✓ No errors (currently FAILS)

Test 3: Site Creation

POST /api/v1/auth/sites/
{
  "name": "Test Site",
  "domain": "https://test.com",
  "industry": 2  # Must be required
}

# Expected:
# ✓ Site created
# ✓ SiteUserAccess created for owner
# ✓ Can add sectors

Test 4: Sector Creation

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

# Expected:
# ✓ Sector created
# ✓ Validation: industry_sector.industry == site.industry
# ✓ Validation: sector count < plan.max_sectors_per_site

📝 Conclusion

Current State: Registration is PARTIALLY BROKEN

  • Free trial signups work (with credit amount issue)
  • Paid plan signups completely broken
  • ⚠️ Site/sector creation has validation issues
  • ⚠️ Data integrity risks from duplicate fields

Estimated Fix Time:

  • Critical fixes: 2-4 hours
  • Medium fixes: 1-2 days
  • Low priority: 1 week

Recommended Approach:

  1. Fix critical import and field issues (Phase 1) - URGENT
  2. Test all signup flows thoroughly
  3. Address medium priority issues incrementally
  4. Plan technical debt cleanup for next quarter

Document Version: 1.0
Next Review: After Phase 1 fixes implemented
Owner: Development Team