# 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**: ```python # In auth/serializers.py line 291 from igny8_core.business.billing.models import Subscription # โŒ IMPORT FAILS ``` **Evidence:** ```python # 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: ```python 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` ```python # 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:** ```python # 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:** ```python # 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: ```python 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: ```python # 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: ```python # 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: ```python # 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:** ```python # Database schema: industry = models.ForeignKey(Igny8Industries, blank=True, null=True) # โŒ NULLABLE ``` **Fix Required:** 1. Make field required: `Site.industry` โ†’ `null=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: ```sql 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:** ```python # 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`: ```json { "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: ```python # 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_industries` โ†’ `max_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:** ```python # 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`: ```python 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:** ```python # During site creation SiteUserAccess.objects.create(user=owner, site=site) ``` **Actual Flow:** ```python # 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: ```python # 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: ```python # 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: ```python 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: ```python # 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: ```python 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: ```python # 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: ```python 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:** ```bash # 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) 6. Duplicate billing_email fields 7. Plan.max_industries misnamed 8. Duplicate payment_method fields (3 places) 9. tenant_id vs account naming confusion 10. SiteUserAccess never created 11. No CreditService for atomic updates --- ### ๐ŸŸข LOW (Technical Debt) 12. Legacy WordPress fields 13. Plan.credits_per_month deprecated 14. No slug generation utility --- ## ๐Ÿ”ง Required Actions Before Production ### Phase 1: Emergency Fixes (Today) ```python # 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) ```sql -- 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 ```bash 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 ```bash 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 ```bash 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 ```bash 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