36 KiB
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
- Free Trial Signup Flow
- Paid Signup Flow
- Payment Confirmation Flow
- Payment Approval Flow (Admin)
- Site Creation Flow
- Sector Selection Flow
- Credit Allocation Flow
- Currency Conversion Flow
- Authentication Flow
- 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:
- Payment status → succeeded
- Invoice status → paid
- Subscription status → active, dates set
- Account status → active
- Credits allocated
- 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.