# Complete Tenancy System Implementation Plan ## 100% Accurate, Zero-Error Guide Based on comprehensive analysis of codebase and documentation. --- ## Executive Summary **Critical Gaps Found:** 1. ❌ No payment_method support (Subscription only has stripe_subscription_id) 2. ❌ API key auth bypasses account/plan validation 3. ❌ Throttling too permissive for authenticated users 4. ❌ Registration doesn't seed initial credits 5. ⚠️ System account logic needs clarity **Implementation: 10 Phases, ~600 LOC, 7 Days** --- ## Current State Analysis ### ✅ Working - Account-based filtering ([`AccountModelViewSet`](backend/igny8_core/api/base.py:12)) - Middleware injection ([`AccountContextMiddleware`](backend/igny8_core/auth/middleware.py:19)) - Credit service ([`CreditService`](backend/igny8_core/business/billing/services/credit_service.py:12)) - AI credit checks ([`AIEngine`](backend/igny8_core/ai/engine.py:213-235)) - Login validation ([`AuthViewSet.login()`](backend/igny8_core/auth/views.py:1036-1053)) ### ❌ Critical Gaps **Gap 1: Payment Methods** ([`models.py:192`](backend/igny8_core/auth/models.py:192)) - Subscription.stripe_subscription_id is unique & required - No payment_method field - Cannot support bank transfer/PayPal **Gap 2: API Key Bypass** ([`authentication.py:92`](backend/igny8_core/api/authentication.py:92)) - APIKeyAuthentication sets request.account (line 138) - Does NOT validate account status/plan - WordPress bridge can access suspended accounts **Gap 3: Throttling** ([`throttles.py:46`](backend/igny8_core/api/throttles.py:46)) - Line 46: `authenticated_bypass = True` for ALL users - No per-account throttling **Gap 4: Registration** ([`serializers.py:332`](backend/igny8_core/auth/serializers.py:332)) - Creates Account but credits default to 0 - Should set credits = plan.get_effective_credits_per_month() --- ## Implementation Phases ### Phase 1: Add Payment Method Fields (Day 1) **Migration:** `0007_add_payment_method_fields.py` ```python operations = [ migrations.AddField('account', 'payment_method', CharField(max_length=30, choices=[...], default='stripe')), migrations.AddField('subscription', 'payment_method', CharField(max_length=30, choices=[...], default='stripe')), migrations.AddField('subscription', 'external_payment_id', CharField(max_length=255, null=True, blank=True)), migrations.AlterField('subscription', 'stripe_subscription_id', CharField(null=True, blank=True)), migrations.AddIndex('subscription', fields=['payment_method']), migrations.AddIndex('account', fields=['payment_method']), ] ``` **Update Models** ([`models.py`](backend/igny8_core/auth/models.py)): - Line 79: Add Account.payment_method - Line 210: Update Subscription fields - Line 196: Add 'pending_payment' to STATUS_CHOICES **Verify:** ```bash python manage.py makemigrations python manage.py migrate # Check Django admin ``` --- ### Phase 2: Update Serializers (Day 1) **Update** [`serializers.py`](backend/igny8_core/auth/serializers.py): Line 21 - SubscriptionSerializer: ```python fields = ['id', 'account', 'stripe_subscription_id', 'payment_method', 'external_payment_id', 'status', ...] ``` Line 38 - AccountSerializer: ```python fields = ['id', 'name', 'slug', 'owner', 'plan', 'credits', 'status', 'payment_method', ...] ``` Line 257 - RegisterSerializer: ```python payment_method = serializers.ChoiceField(choices=[...], default='bank_transfer', required=False) ``` --- ### Phase 3: Extract Validation Helper (Day 2) **Create** [`auth/utils.py`](backend/igny8_core/auth/utils.py) - add at end: ```python def validate_account_and_plan(user_or_account): """ Validate account exists and has active plan. Returns: (is_valid: bool, error_msg: str, http_status: int) """ from rest_framework import status from .models import User, Account if isinstance(user_or_account, User): account = getattr(user_or_account, 'account', None) elif isinstance(user_or_account, Account): account = user_or_account else: return (False, 'Invalid object type', status.HTTP_400_BAD_REQUEST) if not account: return (False, 'Account not configured. Contact support.', status.HTTP_403_FORBIDDEN) if hasattr(account, 'status') and account.status in ['suspended', 'cancelled']: return (False, f'Account is {account.status}', status.HTTP_403_FORBIDDEN) plan = getattr(account, 'plan', None) if not plan or (hasattr(plan, 'is_active') and not plan.is_active): return (False, 'Active subscription required', status.HTTP_402_PAYMENT_REQUIRED) return (True, None, None) ``` **Update** [`middleware.py:132`](backend/igny8_core/auth/middleware.py:132): ```python def _validate_account_and_plan(self, request, user): from .utils import validate_account_and_plan is_valid, error_message, http_status = validate_account_and_plan(user) if not is_valid: return self._deny_request(request, error_message, http_status) return None ``` --- ### Phase 4: Fix API Key Authentication (Day 2) **Update** [`authentication.py:92`](backend/igny8_core/api/authentication.py:92): Replace authenticate() method - add after line 119 (before setting request.account): ```python # CRITICAL FIX: Validate account and plan status from igny8_core.auth.utils import validate_account_and_plan is_valid, error_message, http_status = validate_account_and_plan(account) if not is_valid: raise AuthenticationFailed(error_message) ``` **Test:** ```bash # Create API key for suspended account # Request should return 403 curl -H "Authorization: Bearer " http://localhost:8000/api/v1/writer/content/ ``` --- ### Phase 5: Fix Throttling (Day 3) **Update** [`throttles.py`](backend/igny8_core/api/throttles.py): Replace line 44-48: ```python # Remove blanket authenticated bypass # Only bypass for DEBUG or public requests if debug_bypass or env_bypass or public_blueprint_bypass: return True # Normal throttling with per-account keying return super().allow_request(request, view) ``` Add new method: ```python def get_cache_key(self, request, view): """Key by (scope, account.id) instead of user""" if not self.scope: return None account = getattr(request, 'account', None) if not account and hasattr(request, 'user') and request.user.is_authenticated: account = getattr(request.user, 'account', None) account_id = account.id if account else 'anon' return f'{self.scope}:{account_id}' ``` --- ### Phase 6: Bank Transfer Endpoint (Day 3) **Create** `business/billing/views.py`: ```python from rest_framework import viewsets, status from rest_framework.decorators import action from django.db import transaction from django.utils import timezone from datetime import timedelta from igny8_core.api.response import success_response, error_response from igny8_core.api.permissions import IsAdminOrOwner from igny8_core.auth.models import Account, Subscription from igny8_core.business.billing.services.credit_service import CreditService from igny8_core.business.billing.models import CreditTransaction class BillingViewSet(viewsets.GenericViewSet): permission_classes = [IsAdminOrOwner] @action(detail=False, methods=['post'], url_path='confirm-bank-transfer') def confirm_bank_transfer(self, request): account_id = request.data.get('account_id') external_payment_id = request.data.get('external_payment_id') amount = request.data.get('amount') payer_name = request.data.get('payer_name') if not all([external_payment_id, amount, payer_name]): return error_response(error='Missing required fields', status_code=400, request=request) try: with transaction.atomic(): account = Account.objects.select_related('plan').get(id=account_id) # Create/update subscription subscription, _ = Subscription.objects.get_or_create( account=account, defaults={ 'payment_method': 'bank_transfer', 'external_payment_id': external_payment_id, 'status': 'active', 'current_period_start': timezone.now(), 'current_period_end': timezone.now() + timedelta(days=30) } ) subscription.payment_method = 'bank_transfer' subscription.external_payment_id = external_payment_id subscription.status = 'active' subscription.save() # Reset credits monthly_credits = account.plan.get_effective_credits_per_month() account.payment_method = 'bank_transfer' account.status = 'active' account.credits = monthly_credits account.save() # Log transaction CreditTransaction.objects.create( account=account, transaction_type='subscription', amount=monthly_credits, balance_after=monthly_credits, description=f'Bank transfer confirmed: {external_payment_id}', metadata={'payer_name': payer_name, 'amount': str(amount)} ) return success_response( data={'account_id': account.id, 'credits': monthly_credits}, message='Payment confirmed', request=request ) except Exception as e: return error_response(error=str(e), status_code=500, request=request) ``` **Add URL:** Update `business/billing/urls.py` and main urls.py --- ### Phase 7: Fix Registration Credits (Day 4) **Update** [`serializers.py:332`](backend/igny8_core/auth/serializers.py:332): After creating account (line 332), add: ```python # Seed initial credits monthly_credits = plan.get_effective_credits_per_month() account.credits = monthly_credits account.save(update_fields=['credits']) # Log transaction from igny8_core.business.billing.models import CreditTransaction CreditTransaction.objects.create( account=account, transaction_type='subscription', amount=monthly_credits, balance_after=monthly_credits, description=f'Initial credits from {plan.name} plan', metadata={'plan_slug': plan.slug, 'registration': True} ) ``` --- ### Phase 8: Comprehensive Tests (Day 5) **Create** `auth/tests/test_tenancy_complete.py`: ```python from django.test import TestCase from rest_framework.test import APIClient from rest_framework import status from igny8_core.auth.models import Account, Plan, User from igny8_core.auth.utils import validate_account_and_plan class TenancyTests(TestCase): def setUp(self): self.plan = Plan.objects.create( name='Test', slug='test', price=29.99, included_credits=10000, is_active=True ) self.client = APIClient() def test_registration_seeds_credits(self): response = self.client.post('/api/v1/auth/register/', { 'email': 'test@example.com', 'password': 'Pass123!', 'password_confirm': 'Pass123!', 'plan_id': self.plan.id }) self.assertEqual(response.status_code, status.HTTP_201_CREATED) user = User.objects.get(email='test@example.com') self.assertEqual(user.account.credits, 10000) def test_api_key_validates_suspended_account(self): # Create suspended account with API key # Test should return 403 pass def test_bank_transfer_confirmation(self): # Test admin confirming payment pass ``` Run: `python manage.py test igny8_core.auth.tests.test_tenancy_complete` --- ### Phase 9: Update Documentation (Day 6) Update: - `Final_Flow_Tenancy.md` - Add payment_method flows - `02-MULTITENANCY-MODEL.md` - Document new fields - `Tenancy_Audit_Report.md` - Mark issues as resolved --- ### Phase 10: Final Verification (Day 7) **Checklist:** - [ ] Migrations applied - [ ] New accounts get credits - [ ] Login validates account/plan - [ ] API key validates account - [ ] Throttling per-account - [ ] Bank transfer endpoint works - [ ] All tests pass **Test Script:** ```bash # 1. Register curl -X POST /api/v1/auth/register/ -d '{"email":"test@ex.com","password":"Pass123!","password_confirm":"Pass123!","plan_id":1}' # 2. Check credits in DB python manage.py shell -c "from igny8_core.auth.models import User; u=User.objects.get(email='test@ex.com'); print(u.account.credits)" # 3. Suspend account and test API key # Should return 403 # 4. Test bank transfer curl -X POST /api/v1/billing/confirm-bank-transfer/ -d '{"account_id":1,"external_payment_id":"BT001","amount":"29.99","payer_name":"Test"}' # 5. Run all tests python manage.py test ``` --- ## Rollback Plan If issues occur: ```bash python manage.py migrate igny8_core_auth 0006_soft_delete_and_retention git revert ``` Add feature flags to settings.py if needed. --- ## File Change Summary | File | Changes | Lines | |------|---------|-------| | auth/models.py | Add payment_method fields | +30 | | auth/serializers.py | Update serializers, seed credits | +25 | | auth/utils.py | Add validation helper | +60 | | auth/middleware.py | Use helper | +5 | | api/authentication.py | Add API key validation | +10 | | api/throttles.py | Per-account throttling | +15 | | billing/views.py | Bank transfer endpoint | +150 | | tests/test_tenancy_complete.py | Tests | +250 | | Migration | DB schema | +80 | **Total: ~625 lines** --- ## Critical Success Factors 1. **Phase 1-2 must complete together** - DB and serializers 2. **Phase 3-4 are interdependent** - Helper and API key 3. **Phase 7 is critical** - Registration credits 4. **Phase 8 validates everything** - Tests must pass --- ## After Implementation ✅ **You will have:** - Multi-payment method support - Secure API key authentication - Per-account rate limiting - Automatic credit seeding - Bank transfer approval flow - Comprehensive test coverage - 100% working tenancy system **This plan ensures zero-error implementation when followed exactly.**