Files
igny8/multi-tenancy/TENANCY-DATA-FLOW.md
IGNY8 VPS (Salman) 6a4f95c35a docs re-org
2025-12-09 13:26:35 +00:00

36 KiB
Raw Blame History

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
  2. Paid Signup Flow
  3. Payment Confirmation Flow
  4. Payment Approval Flow (Admin)
  5. Site Creation Flow
  6. Sector Selection Flow
  7. Credit Allocation Flow
  8. Currency Conversion Flow
  9. Authentication Flow
  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

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

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.