Files
igny8/docs/multi-tenancy/TENANCY-DATA-FLOW.md
IGNY8 VPS (Salman) 4dd129b863 asda
2025-12-09 13:27:29 +00:00

1427 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.