1427 lines
36 KiB
Markdown
1427 lines
36 KiB
Markdown
# 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.
|