# IGNY8 Multi-Tenancy Data Flow Diagrams **Last Updated:** December 9, 2024 **Document Purpose:** Visual representation of all critical workflows in IGNY8 multi-tenancy system **Companion Document:** TENANCY-IMPLEMENTATION-GUIDE.md --- ## Table of Contents 1. [Free Trial Signup Flow](#1-free-trial-signup-flow) 2. [Paid Signup Flow](#2-paid-signup-flow) 3. [Payment Confirmation Flow](#3-payment-confirmation-flow) 4. [Payment Approval Flow (Admin)](#4-payment-approval-flow-admin) 5. [Site Creation Flow](#5-site-creation-flow) 6. [Sector Selection Flow](#6-sector-selection-flow) 7. [Credit Allocation Flow](#7-credit-allocation-flow) 8. [Currency Conversion Flow](#8-currency-conversion-flow) 9. [Authentication Flow](#9-authentication-flow) 10. [Complete End-to-End Journey](#10-complete-end-to-end-journey) --- ## 1. Free Trial Signup Flow ### Flow Diagram ``` User Visits Signup Page ↓ Enters User Details - Email - Password - First Name - Last Name - Country Selection ↓ Selects "Free Trial" Plan ↓ Frontend → POST /v1/auth/register/ ↓ Backend: RegisterView.post() ├─ Validate Email Uniqueness ├─ Validate Password Strength └─ Check Country Code ↓ Database Transaction Starts ↓ Create Account Record - Generate UUID - billing_country = selected country - account_status = 'trial' - credits = 0 (initially) ↓ Create User Record - Set account FK - Set role = 'owner' - hash password - is_active = True ↓ Fetch Free Trial Plan - Filter: plan_type='trial' - Get default trial plan ↓ Create Subscription - account FK - plan FK - status = 'active' - current_period_start = now - current_period_end = now + 7 days - auto_renew = False ↓ Allocate Trial Credits - Create CreditTransaction - type = 'subscription' - amount = plan.credits (e.g., 500) - description = "Free trial credits" - Update account.credits = 500 ↓ Generate JWT Tokens - Access token (15 min expiry) - Refresh token (7 day expiry) - Include account_id in claims ↓ Commit Transaction ↓ Response to Frontend { user: {...}, account: {...}, subscription: {...}, tokens: { access: "...", refresh: "..." } } ↓ Frontend: authStore.setAuth() - Store tokens in localStorage - Set user state in Zustand - Set account state ↓ Redirect to Dashboard - Free trial banner visible - 500 credits available - 7 days remaining ``` ### Key Decision Points **Q: Email already exists?** - Yes → Return 400 error "Email already registered" - No → Continue **Q: Country has payment methods?** - Yes → Store billing_country - No → Default to 'US', store billing_country **Q: Free trial plan exists?** - Yes → Use existing plan - No → Return 500 error "Free trial plan not configured" ### State Changes | Entity | Before | After | |--------|--------|-------| | Account | Does not exist | account_status='trial', credits=500 | | User | Does not exist | is_active=True, role='owner' | | Subscription | Does not exist | status='active', 7-day period | | Credits | N/A | 500 credits from plan | --- ## 2. Paid Signup Flow ### Flow Diagram ``` User Visits Signup Page ↓ Enters User Details - Email, Password, Name, Country ↓ Selects Paid Plan - Basic ($29/month) - Pro ($79/month) - Enterprise ($199/month) ↓ Frontend → POST /v1/auth/register/ ↓ Backend: RegisterView.post() ├─ Validate User Data └─ Detect plan_type = 'paid' ↓ Database Transaction Starts ↓ Create Account Record - billing_country = selected country - account_status = 'pending_payment' - credits = 0 ↓ Create User Record - Set account FK - role = 'owner' - is_active = True ↓ Convert Plan Price to Local Currency - Fetch plan.price_usd (e.g., $29) - Get currency multiplier for country - Calculate: local_amount = price_usd × multiplier - Example: $29 × 278 = PKR 8,062 ↓ Create Subscription - account FK - plan FK - status = 'incomplete' - current_period_start = None - current_period_end = None - auto_renew = True (for paid plans) ↓ Create Invoice - account FK - subscription FK - amount = local_amount (PKR 8,062) - currency = local currency code ('PKR') - status = 'pending' - due_date = now + 7 days - Metadata: - usd_price: $29 - exchange_rate: 278 - plan_name: "Basic Plan" ↓ NO Credit Allocation Yet (Credits allocated after payment approval) ↓ Generate JWT Tokens - Access token - Refresh token ↓ Commit Transaction ↓ Response to Frontend { user: {...}, account: { account_status: 'pending_payment', credits: 0 }, subscription: { status: 'incomplete' }, invoice: { invoice_number: "INV-123", total: 8062.00, currency: "PKR", status: "pending" }, tokens: {...} } ↓ Frontend: authStore.setAuth() - Store all data - Detect pending_payment status ↓ Redirect to Payment Instructions - Show invoice details - Display payment methods for country - Request manual payment confirmation ``` ### Critical Differences from Free Trial | Aspect | Free Trial | Paid Signup | |--------|-----------|-------------| | Account Status | 'trial' | 'pending_payment' | | Subscription Status | 'active' | 'incomplete' | | Credits on Signup | Allocated immediately | 0 (allocated after payment) | | Invoice Created | No | Yes | | Period Dates | Set immediately | null until payment | --- ## 3. Payment Confirmation Flow ### Flow Diagram ``` User Completes Manual Payment (Bank Transfer or Mobile Wallet) ↓ User Opens Payment Modal - Frontend fetches invoice - GET /v1/billing/invoices/ ↓ Display Invoice Details - Amount: PKR 8,062.00 - Currency: PKR - Plan: Basic Plan ↓ User Selects Payment Method - Dropdown shows methods for billing_country - GET /v1/billing/admin/payment-methods/?country=PK - Returns: [JazzCash, EasyPaisa, Bank Transfer] ↓ User Enters Payment Details - Selected method: JazzCash - Transaction ID: JC20241209123456 - Notes: "Paid via JazzCash mobile app" - Amount: 8062.00 (auto-filled, read-only) ↓ Frontend → POST /v1/billing/admin/payments/confirm/ { invoice_id: 123, payment_method_id: 45, manual_reference: "JC20241209123456", manual_notes: "Paid via JazzCash mobile app", amount: 8062.00 } ↓ Backend: AdminPaymentView.confirm_payment() ├─ Validate invoice exists ├─ Validate invoice belongs to user's account ├─ Check invoice status = 'pending' └─ Validate amount matches invoice.total exactly ↓ Database Transaction Starts ↓ Create Payment Record - invoice FK - account FK - payment_method FK - amount = 8062.00 - currency = 'PKR' - status = 'pending_approval' - manual_reference = "JC20241209123456" - manual_notes = user's notes - confirmed_at = now() ↓ Update Invoice Status - status = 'pending' → 'pending_approval' - Indicates payment under review ↓ Commit Transaction ↓ Response to Frontend { payment: { id: 789, status: 'pending_approval', amount: 8062.00, manual_reference: "JC20241209123456" }, message: "Payment submitted for approval" } ↓ Frontend Shows Success - "Payment submitted successfully" - "Admin will review within 24 hours" - Close modal - Update invoice list ↓ User Waits for Admin Approval ``` ### Validation Rules | Field | Validation | |-------|-----------| | invoice_id | Must exist, belong to user's account | | payment_method_id | Must exist, be active for user's country | | amount | Must exactly match invoice.total | | manual_reference | Required, max 255 chars | | manual_notes | Optional, max 1000 chars | ### State Changes | Entity | Before | After | |--------|--------|-------| | Invoice | status='pending' | status='pending_approval' | | Payment | Does not exist | status='pending_approval' | | Account | account_status='pending_payment' | No change yet | | Subscription | status='incomplete' | No change yet | --- ## 4. Payment Approval Flow (Admin) ### Flow Diagram ``` Admin Logs into Django Admin ↓ Navigates to Billing → Payments ↓ Sees Payment List - Filters by status='pending_approval' - Shows all pending manual payments ↓ Admin Reviews Payment - Checks manual_reference (transaction ID) - Verifies amount matches invoice - Reviews manual_notes from user - Confirms payment received in bank/wallet ↓ Admin Approves Payment Option A: Bulk Action ├─ Selects payment checkboxes ├─ Chooses "Approve selected manual payments" ├─ Clicks "Go" └─ Calls approve_manual_payments() action Option B: Individual Edit ├─ Clicks payment to open ├─ Changes Status → "Succeeded" ├─ Adds admin notes (optional) ├─ Clicks "Save" └─ Calls PaymentAdmin.save_model() ↓ Backend Detects Status Change - old_status = 'pending_approval' - new_status = 'succeeded' - Change detected ↓ Database Transaction Starts (Atomic) ↓ Update Payment Record - status = 'succeeded' - approved_by = admin user - approved_at = now() ↓ Fetch Related Invoice - Get payment.invoice ↓ Update Invoice - status = 'pending_approval' → 'paid' - paid_at = now() ↓ Fetch Subscription - Get invoice.subscription ↓ Update Subscription - status = 'incomplete' → 'active' - current_period_start = now() - current_period_end = now() + 1 month (for monthly plans) ↓ Fetch Account - Get subscription.account ↓ Update Account Status - account_status = 'pending_payment' → 'active' ↓ Allocate Credits (via CreditService) ├─ Get plan.credits (e.g., 5000 for Basic) ├─ Create CreditTransaction │ - type = 'subscription' │ - amount = 5000 │ - description = "Credits for Basic Plan subscription" │ - metadata = {payment_id, invoice_id} │ - balance_after = 5000 ├─ Update account.credits = 5000 └─ Return transaction record ↓ Check for Duplicate Allocations - Query CreditTransaction - Filter by payment_id in metadata - If already exists: Skip allocation - Prevents double-credits on repeated saves ↓ Commit Transaction ↓ Admin Sees Success Message "Payment approved successfully. Account activated with 5000 credits." ↓ User Can Now Access Full Platform - account_status = 'active' - subscription.status = 'active' - 5000 credits available - All features unlocked ``` ### Approval Decision Tree ``` Payment Status = 'pending_approval' ↓ Admin Reviews Evidence ├─ Transaction ID valid? ────→ No → Reject Payment ├─ Amount matches invoice? ──→ No → Reject Payment ├─ Payment received? ────────→ No → Keep pending └─ All checks pass? ─────────→ Yes → Approve Payment ↓ Automatic Workflow ↓ Account Activated ``` ### Critical Atomic Updates All these must succeed together or all fail: 1. Payment status → succeeded 2. Invoice status → paid 3. Subscription status → active, dates set 4. Account status → active 5. Credits allocated 6. Transaction recorded If any step fails: Entire transaction rolled back --- ## 5. Site Creation Flow ### Flow Diagram ``` User Navigates to Dashboard ↓ Clicks "Add Site" Button ↓ Opens Site Creation Wizard ↓ Step 1: Basic Information - Enter site name (e.g., "Tech Blog") - Enter domain (optional, e.g., "https://techblog.com") - Enter description (optional) ↓ Step 2: Industry Selection - Dropdown shows all industries - User selects (e.g., "Technology") - Frontend stores industry.id ↓ Step 3: Additional Settings - Select hosting type (WordPress/Custom/Static) - Select site type (Blog/Ecommerce/Corporate) - Toggle is_active (default: true) ↓ User Clicks "Create Site" ↓ Frontend → POST /v1/auth/sites/ { name: "Tech Blog", domain: "https://techblog.com", description: "A blog about technology", industry: 5, hosting_type: "wordpress", site_type: "blog", is_active: true } ↓ Backend: SiteViewSet.create() ↓ Middleware Sets request.account - Extract from JWT token - Attach to request object ↓ Check Site Limit ├─ Get account.plan.max_sites (e.g., 3 for Basic) ├─ Count active sites for account ├─ Current count >= limit? │ ├─ Yes → Return 400 "Site limit reached for your plan" │ └─ No → Continue ↓ Auto-Generate Slug ├─ slugify(name) → "tech-blog" ├─ Check uniqueness within account ├─ If exists: Append number "tech-blog-2" └─ Final slug: "tech-blog" ↓ Validate and Format Domain ├─ domain = "https://techblog.com" ├─ Strip whitespace ├─ If empty string → Set to None ├─ If starts with http:// → Replace with https:// ├─ If no protocol → Add https:// └─ Validate URL format ↓ Database Transaction Starts ↓ Create Site Record - account FK (from request.account) - name = "Tech Blog" - slug = "tech-blog" - domain = "https://techblog.com" - description = text - industry FK = 5 - hosting_type = "wordpress" - site_type = "blog" - is_active = True - created_by = request.user ↓ Check User Role ├─ role in ['owner', 'admin']? │ ├─ Yes → Grant Access │ │ └─ Create SiteUserAccess │ │ - user FK │ │ - site FK │ │ - granted_by = request.user │ └─ No → Skip access grant ↓ Commit Transaction ↓ Response to Frontend { id: 123, name: "Tech Blog", slug: "tech-blog", domain: "https://techblog.com", industry: { id: 5, name: "Technology", slug: "technology" }, sectors_count: 0, is_active: true } ↓ Frontend Updates Site List - Add new site to state - Close wizard modal - Show success message ↓ Redirect to Sector Selection - Prompt: "Select up to 5 sectors for your site" ``` ### Site Creation Validations | Validation | Rule | Error Message | |-----------|------|---------------| | Plan Limit | active_sites < max_sites | "Site limit reached for your plan" | | Name | Required, max 255 chars | "Site name is required" | | Slug | Unique within account | Auto-appends number if duplicate | | Domain | Valid URL or None | "Invalid domain format" | | Industry | Must exist | "Invalid industry selected" | ### State Changes | Entity | Before | After | |--------|--------|-------| | Site | Does not exist | is_active=True, sectors_count=0 | | SiteUserAccess | Does not exist | Created for owner/admin | | Account | site_count = n | site_count = n+1 | --- ## 6. Sector Selection Flow ### Flow Diagram ``` User on Site Details Page ↓ Clicks "Add Sectors" Button ↓ Frontend Fetches Available Sectors GET /v1/auth/industries/{industry_slug}/sectors/ ↓ Response: List of IndustrySectors [ {id: 1, name: "AI & Machine Learning", slug: "ai-ml"}, {id: 2, name: "Web Development", slug: "web-dev"}, {id: 3, name: "Mobile Apps", slug: "mobile-apps"}, ... ] ↓ Display Sector Selection UI - Show checkboxes for each sector - Limit: "Select up to 5 sectors" - Track selected count ↓ User Selects Sectors - Checks 3 sectors: - AI & Machine Learning - Web Development - Cloud Computing ↓ User Clicks "Save Sectors" ↓ Frontend → POST /v1/auth/sites/{site_id}/select_sectors/ { industry_slug: "technology", sector_slugs: ["ai-ml", "web-dev", "cloud-computing"] } ↓ Backend: SiteViewSet.select_sectors() ↓ Validate Site Ownership ├─ site.account == request.account? │ ├─ Yes → Continue │ └─ No → Return 403 "Access denied" ↓ Validate Industry Match ├─ Fetch industry by slug ├─ industry == site.industry? │ ├─ Yes → Continue │ └─ No → Return 400 "Industry mismatch" ↓ Check Sector Limit ├─ Max sectors per site = 5 ├─ Count current active sectors ├─ Current count = 0 ├─ Requested count = 3 ├─ Available slots = 5 - 0 = 5 ├─ 3 <= 5? │ ├─ Yes → Continue │ └─ No → Return 400 "Sector limit exceeded" ↓ Fetch IndustrySectors ├─ Query by slugs: ["ai-ml", "web-dev", "cloud-computing"] ├─ Filter by industry ├─ Validate all 3 exist └─ All found → Continue ↓ Database Transaction Starts ↓ For Each IndustrySector: ↓ Check if Sector Already Exists ├─ Query Sector by site + industry_sector ├─ Found existing sector? │ ├─ Yes → Update │ │ └─ Set is_active = True │ │ └─ updated_sectors++ │ └─ No → Create │ └─ New Sector Record │ - site FK │ - industry_sector FK │ - account FK (from site.account) │ - name = industry_sector.name │ - slug = industry_sector.slug │ - is_active = True │ - created_by = request.user │ └─ created_sectors++ ↓ Commit Transaction ↓ Response to Frontend { created: 3, updated: 0, sectors: [ { id: 501, name: "AI & Machine Learning", slug: "ai-ml", is_active: true }, { id: 502, name: "Web Development", slug: "web-dev", is_active: true }, { id: 503, name: "Cloud Computing", slug: "cloud-computing", is_active: true } ] } ↓ Frontend Updates UI - Display selected sectors - Update sectors_count = 3 - Show success message - Enable "Next" button in wizard ``` ### Sector Limit Logic ``` Max Sectors = 5 (Hard-coded) ↓ Count Active Sectors for Site ↓ Available Slots = Max - Current ↓ Validate Request ├─ Requested <= Available? │ ├─ Yes → Allow │ └─ No → Reject ``` ### Example Scenarios **Scenario 1: New Site (0 sectors)** - Request: 3 sectors - Current: 0 - Available: 5 - Result: ✅ Allow, create 3 new sectors **Scenario 2: Site with 3 Sectors** - Request: 2 more sectors - Current: 3 - Available: 2 - Result: ✅ Allow, create 2 new sectors **Scenario 3: Site with 5 Sectors** - Request: 1 more sector - Current: 5 - Available: 0 - Result: ❌ Reject "Sector limit reached" **Scenario 4: Reactivating Deleted Sector** - Request: sector-a (previously deleted) - Query finds existing Sector with is_active=False - Result: ✅ Update is_active=True (no new creation) --- ## 7. Credit Allocation Flow ### Flow Diagram ``` Trigger Event ├─ Payment Approval (Admin) ├─ Free Trial Signup ├─ Manual Credit Adjustment (Admin) └─ Subscription Renewal (Auto) ↓ Call: CreditService.add_credits() Parameters: - account: Account object - amount: Integer (e.g., 5000) - transaction_type: String ('subscription', 'adjustment', 'refund') - description: String (human-readable) - metadata: Dict (payment_id, invoice_id, etc.) ↓ Validate Amount ├─ amount > 0? │ ├─ Yes → Continue │ └─ No → Raise ValueError "Amount must be positive" ↓ Get Current Balance - current_balance = account.credits - Example: current_balance = 0 ↓ Calculate New Balance - new_balance = current_balance + amount - Example: new_balance = 0 + 5000 = 5000 ↓ Database Transaction Starts ↓ Create CreditTransaction Record - account FK - type = transaction_type ('subscription') - amount = 5000 (positive for additions) - balance_after = 5000 - description = "Credits for Basic Plan subscription" - metadata = { payment_id: 789, invoice_id: 123, plan_name: "Basic Plan" } - created_at = now() ↓ Update Account Credits - account.credits = new_balance (5000) - account.save() ↓ Commit Transaction ↓ Return CreditTransaction Object ↓ Credits Available for Use ``` ### Credit Deduction Flow ``` Content Generation Request ↓ Check Required Credits - Feature: Blog post generation - Cost: 100 credits ↓ Call: CreditService.deduct_credits() Parameters: - account: Account object - amount: 100 - transaction_type: 'content_generation' - description: "Blog post: How to Start a Business" - metadata: {content_id: 456, feature: 'blog'} ↓ Validate Sufficient Balance ├─ account.credits >= amount? │ ├─ Yes → Continue │ └─ No → Raise InsufficientCreditsError ↓ Calculate New Balance - current_balance = 5000 - new_balance = 5000 - 100 = 4900 ↓ Database Transaction Starts ↓ Create CreditTransaction Record - account FK - type = 'content_generation' - amount = -100 (negative for deductions) - balance_after = 4900 - description = "Blog post: How to Start a Business" - metadata = {content_id: 456} ↓ Update Account Credits - account.credits = 4900 - account.save() ↓ Commit Transaction ↓ Return CreditTransaction Object ↓ Credits Deducted, Content Generated ``` ### Transaction History View ``` User Views Credit History GET /v1/billing/credit-transactions/ ↓ Backend Returns List [ { id: 1001, type: "subscription", amount: 5000, balance_after: 5000, description: "Credits for Basic Plan subscription", created_at: "2024-12-09T10:00:00Z" }, { id: 1002, type: "content_generation", amount: -100, balance_after: 4900, description: "Blog post: How to Start a Business", created_at: "2024-12-09T11:00:00Z" }, { id: 1003, type: "content_generation", amount: -50, balance_after: 4850, description: "Social media post batch", created_at: "2024-12-09T12:00:00Z" } ] ↓ Frontend Displays Timeline - Shows running balance - Color codes: Green for additions, Red for deductions - Filterable by type ``` --- ## 8. Currency Conversion Flow ### Flow Diagram ``` User Signs Up from Pakistan ↓ Frontend Detects Country - navigator.language or IP geolocation - country_code = 'PK' ↓ Sends to Backend POST /v1/auth/register/ { ...user data, country: "PK" } ↓ Backend Receives Request ↓ User Selects Plan - Plan: Basic - plan.price_usd = 29.00 ↓ Call: convert_usd_to_local(29.00, 'PK') ↓ Lookup Currency Multiplier ```python CURRENCY_MULTIPLIERS = { 'PK': 278.0, 'IN': 83.0, 'GB': 0.79, ... } multiplier = CURRENCY_MULTIPLIERS.get('PK', 1.0) # multiplier = 278.0 ``` ↓ Calculate Local Amount - local_amount = 29.00 × 278.0 - local_amount = 8,062.00 ↓ Get Currency Code - Call: get_currency_for_country('PK') - Returns: 'PKR' ↓ Create Invoice with Local Currency - amount = 8062.00 - currency = 'PKR' - metadata = { usd_price: 29.00, exchange_rate: 278.0, country: 'PK' } ↓ Return to Frontend { invoice: { total: 8062.00, currency: "PKR", ... } } ↓ Frontend Displays "Amount Due: PKR 8,062.00" ``` ### Multi-Currency Support Table | Country | Currency | Rate | $29 Plan | $79 Plan | $199 Plan | |---------|----------|------|----------|----------|-----------| | Pakistan | PKR | 278.0 | ₨8,062 | ₨21,962 | ₨55,322 | | India | INR | 83.0 | ₹2,407 | ₹6,557 | ₹16,517 | | UK | GBP | 0.79 | £22.91 | £62.41 | £157.21 | | Europe | EUR | 0.92 | €26.68 | €72.68 | €183.08 | | Canada | CAD | 1.36 | C$39.44 | C$107.44 | C$270.64 | | Australia | AUD | 1.52 | A$44.08 | A$120.08 | A$302.48 | | USA | USD | 1.0 | $29.00 | $79.00 | $199.00 | ### Currency Display Formatting ``` Call: format_currency(8062.00, 'PKR') ↓ Returns: "PKR 8,062.00" Call: format_currency(29.00, 'USD') ↓ Returns: "$29.00" Call: format_currency(2407.00, 'INR') ↓ Returns: "₹2,407.00" ``` --- ## 9. Authentication Flow ### Registration with JWT Token Generation ``` User Submits Registration POST /v1/auth/register/ ↓ Backend: RegisterView.post() ↓ Create User & Account (See Free Trial or Paid Signup Flow) ↓ Generate Access Token ```python from igny8_core.auth.utils import generate_access_token access_token = generate_access_token(user) # Creates JWT with: # - user_id # - account_id # - email # - role # - exp: 15 minutes from now ``` ↓ Generate Refresh Token ```python from igny8_core.auth.utils import generate_refresh_token refresh_token = generate_refresh_token(user) # Creates JWT with: # - user_id # - exp: 7 days from now ``` ↓ Return Tokens in Response { user: {...}, account: {...}, tokens: { access: "eyJhbGciOi...", refresh: "eyJhbGciOi..." } } ↓ Frontend Receives Response ↓ authStore.setAuth() Called ├─ Extract tokens from response │ └─ Check multiple paths: │ - tokens?.access │ - data?.tokens?.access │ - data?.data?.tokens?.access ├─ Store in localStorage (synchronous) │ └─ localStorage.setItem('access_token', token) │ └─ localStorage.setItem('refresh_token', token) └─ Update Zustand state └─ set({ user, account, isAuthenticated: true }) ↓ User Authenticated and Logged In ``` **Key Fix Applied (Dec 9, 2024):** Problem: Users logged out immediately after signup - RegisterView didn't generate JWT tokens - Frontend couldn't find tokens in response - authStore set user but no tokens - Subsequent requests failed authentication Solution: Generate tokens like LoginView - Added generate_access_token(user) call - Added generate_refresh_token(user) call - Return tokens in response data - Frontend extracts with fallback paths --- ### Login Flow ``` User Enters Credentials POST /v1/auth/login/ { email: "user@example.com", password: "SecurePass123" } ↓ Backend: LoginView.post() ↓ Validate Credentials ├─ User exists with email? ├─ Password hash matches? ├─ User is_active = True? │ ├─ All pass → Continue │ └─ Any fail → Return 401 "Invalid credentials" ↓ Check Account Status ├─ account.account_status in ['active', 'trial']? │ ├─ Yes → Continue │ └─ No → Return 403 "Account not active" ↓ Generate Tokens - Access token (15 min) - Refresh token (7 days) ↓ Return Response { user: {...}, account: {...}, tokens: { access: "...", refresh: "..." } } ↓ Frontend Stores Tokens - localStorage persistence - Zustand state update ↓ User Logged In ``` ### Token Refresh Flow ``` Access Token Expires (15 min) ↓ API Request Fails with 401 ↓ Frontend Detects Expiration ↓ Check Refresh Token ├─ Refresh token exists? ├─ Refresh token not expired? │ ├─ Yes → Refresh │ └─ No → Force logout ↓ POST /v1/auth/refresh/ { refresh: "eyJhbGciOi..." } ↓ Backend Validates Refresh Token ├─ Token signature valid? ├─ Token not expired? ├─ User still exists and active? │ ├─ All pass → Continue │ └─ Any fail → Return 401 ↓ Generate New Access Token - Fresh 15-minute token - Same claims as before ↓ Return Response { access: "eyJhbGciOi...", refresh: "eyJhbGciOi..." (same or new) } ↓ Frontend Updates Token - Replace access_token in localStorage - Retry failed API request ↓ Request Succeeds ``` ### Middleware Flow (Every Request) ``` API Request with Token Authorization: Bearer eyJhbGciOi... ↓ Django Middleware Pipeline ↓ JWTAuthenticationMiddleware ├─ Extract token from header ├─ Decode and validate JWT ├─ Extract user_id from claims ├─ Fetch User object ├─ Attach to request.user └─ Continue to next middleware ↓ TenantMiddleware ├─ Get account_id from JWT claims ├─ Fetch Account object ├─ Attach to request.account ├─ Set thread-local for query filtering └─ Continue to view ↓ View Executes - Has access to request.user - Has access to request.account - All queries auto-filtered by account ``` --- ## 10. Complete End-to-End Journey ### Paid User Journey (Bank Transfer Payment) ``` ┌─────────────────────────────────────────────────────────────┐ │ Day 1: User Signup │ └─────────────────────────────────────────────────────────────┘ ↓ User Visits Landing Page (Pakistan) ↓ Clicks "Get Started" → Signup Form ↓ Fills Form: - Email: owner@business.pk - Password: SecurePass123 - Name: Ahmad Khan - Country: Pakistan ↓ Selects Plan: Basic ($29/month) ↓ POST /v1/auth/register/ ↓ Backend Creates: ├─ Account (account_status='pending_payment') ├─ User (role='owner') ├─ Subscription (status='incomplete') └─ Invoice (PKR 8,062.00, status='pending') ↓ JWT Tokens Generated and Returned ↓ Frontend Redirects to Payment Instructions ↓ User Sees: - Invoice #INV-001 - Amount: PKR 8,062.00 - Payment methods for Pakistan - Instructions for bank transfer ┌─────────────────────────────────────────────────────────────┐ │ Day 1: User Makes Payment │ └─────────────────────────────────────────────────────────────┘ ↓ User Opens Banking App ↓ Transfers PKR 8,062 to Company Account - Transaction ID: TXN20241209001 - Takes screenshot of receipt ↓ Returns to Platform ↓ Clicks "Confirm Payment" Button ↓ Payment Modal Opens - Shows invoice amount: PKR 8,062.00 - Dropdown: Selects "Bank Transfer" - Enters Transaction ID: TXN20241209001 - Enters Notes: "Paid via HBL mobile banking" ↓ POST /v1/billing/admin/payments/confirm/ ↓ Backend Creates Payment: - status='pending_approval' - manual_reference='TXN20241209001' ↓ Invoice Updated: - status='pending' → 'pending_approval' ↓ User Sees Success Message: "Payment submitted. We'll review within 24 hours." ↓ User Waits (Limited Access) - Can browse dashboard - Cannot create sites/content - account_status='pending_payment' ┌─────────────────────────────────────────────────────────────┐ │ Day 2: Admin Approves Payment │ └─────────────────────────────────────────────────────────────┘ ↓ Admin Logs into Django Admin ↓ Navigates: Billing → Payments ↓ Filters: Status = Pending Approval ↓ Sees Payment: - Account: Ahmad Khan (owner@business.pk) - Amount: PKR 8,062.00 - Method: Bank Transfer - Reference: TXN20241209001 - Notes: "Paid via HBL mobile banking" ↓ Admin Verifies: ✓ Checks bank account ✓ Sees PKR 8,062 received ✓ Transaction ID matches ✓ Amount correct ↓ Admin Clicks Payment → Opens Edit ↓ Changes Status: "Pending Approval" → "Succeeded" ↓ Clicks "Save" ↓ Automatic Workflow Triggers: ├─ Payment.status = 'succeeded' ├─ Payment.approved_by = admin ├─ Invoice.status = 'paid' ├─ Subscription.status = 'active' ├─ Subscription.current_period_end = +30 days ├─ Account.account_status = 'active' └─ Credits allocated: +5000 └─ CreditTransaction created ↓ Admin Sees Success Message: "Payment approved. Account activated with 5000 credits." ┌─────────────────────────────────────────────────────────────┐ │ Day 2: User Gets Activated (Automatic) │ └─────────────────────────────────────────────────────────────┘ ↓ User Refreshes Dashboard ↓ API Calls Return Updated Data: - account.account_status = 'active' - account.credits = 5000 - subscription.status = 'active' ↓ UI Changes: - Payment pending banner disappears - "Create Site" button enabled - Credit balance shows 5000 - All features unlocked ↓ User Clicks "Create Site" ↓ Opens Site Creation Wizard ↓ Fills Form: - Name: "Digital Marketing Blog" - Domain: https://digitalmktg.pk - Industry: Marketing - Type: Blog ↓ POST /v1/auth/sites/ ↓ Backend: ├─ Checks plan limit (3 sites for Basic) ├─ Current sites: 0 ├─ Creates site └─ Grants owner access ↓ User Sees New Site Created ↓ Proceeds to Select Sectors: - Selects 3 sectors: ├─ Content Marketing ├─ Social Media └─ SEO ↓ POST /v1/auth/sites/123/select_sectors/ ↓ Backend Creates 3 Sector Records ↓ Site Setup Complete ↓ User Navigates to Content Generation ↓ Generates First Blog Post - Cost: 100 credits - Deduction: 5000 → 4900 ↓ Credits Deducted, Content Created ↓ User Actively Using Platform - 4900 credits remaining - 29 days left in subscription - Full access to all features ┌─────────────────────────────────────────────────────────────┐ │ Day 32: Subscription Renewal (Future Feature) │ └─────────────────────────────────────────────────────────────┘ ↓ Celery Task: check_expiring_subscriptions ↓ Detects subscription.current_period_end approaching ↓ If auto_renew=True: ├─ Create new invoice ├─ Send payment reminder email └─ User repeats payment flow ↓ If auto_renew=False: ├─ Subscription expires ├─ account_status = 'trial' or 'inactive' └─ User must renew manually ``` --- **End of Data Flow Diagrams Document** This document provides visual representation of all critical workflows in the IGNY8 multi-tenancy system as implemented on December 9, 2024.