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.Subscriptionmodel atigny8_core/auth/models.pyline 218- Database table:
igny8_subscriptions(created byauth.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:
-
Option A (Recommended): Use existing
auth.Subscription# Change line 291 in auth/serializers.py from igny8_core.auth.models import Subscription -
Option B: Create
billing.Subscriptionand 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
- Create model in
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:
- Make field required:
Site.industry→null=False, blank=False - Migration: Set default industry for existing NULL sites
- 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:
Account.billing_email- Primary billing contactInvoice.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_emailas 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:
- Rename field:
max_industries→max_sectors_per_site - Migration to rename column
- Update all references in code
- Use
0to mean unlimited
8. Duplicate Subscription Payment Method
Problem:
Payment method stored in THREE places:
Account.payment_method- Account defaultSubscription.payment_method- Subscription payment methodAccountPaymentMethod.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
AccountPaymentMethodas single source of truth - Deprecate
Account.payment_methodandSubscription.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 usestenant_id - Debugging confusion: "Why isn't
account=1working in SQL?" - Must use:
SELECT * FROM igny8_sites WHERE tenant_id=1(notaccount=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_monthfield - 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)
- ❌ billing.Subscription does not exist - Paid signups FAIL
- ❌ Subscription.plan field missing - Invoice creation broken
- ⚠️ Account.owner circular dependency - Race condition risk
- ⚠️ Site.industry is nullable - Sector creation fails
- ⚠️ 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)
- Duplicate billing_email fields
- Plan.max_industries misnamed
- Duplicate payment_method fields (3 places)
- tenant_id vs account naming confusion
- SiteUserAccess never created
- No CreditService for atomic updates
🟢 LOW (Technical Debt)
- Legacy WordPress fields
- Plan.credits_per_month deprecated
- 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)
- Create CreditService
- Auto-create SiteUserAccess
- Rename max_industries → max_sectors_per_site
- 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:
- Fix critical import and field issues (Phase 1) - URGENT
- Test all signup flows thoroughly
- Address medium priority issues incrementally
- Plan technical debt cleanup for next quarter
Document Version: 1.0
Next Review: After Phase 1 fixes implemented
Owner: Development Team