Files
igny8/multi-tenancy/in-progress/COMPLETE-TENANCY-FLOW-DOCUMENTATION.md
2025-12-09 02:43:51 +00:00

98 KiB
Raw Blame History

Complete Tenancy Flow Documentation

Date Created: December 9, 2024
Purpose: Complete end-to-end flow documentation for both Free Trial and Paid Plan user journeys
From: User Registration → Site Creation
System: IGNY8 Multi-Tenant Platform


Table of Contents

  1. Overview
  2. System Architecture
  3. Flow A: Free Trial User Journey
  4. Flow B: Paid Plan User Journey
  5. Database Schema
  6. API Reference
  7. State Transitions
  8. Error Handling
  9. Testing Scenarios

Overview

Purpose

This document provides a comprehensive walkthrough of the complete user registration and onboarding flows for the IGNY8 multi-tenant platform. It covers two distinct paths:

  1. Free Trial Path: Users who sign up for a free trial account
  2. Paid Plan Path: Users who sign up for a paid subscription plan

Key Differences

Aspect Free Trial Paid Plan
Initial Status active (immediate) pending_payment (requires payment)
Credits Allocated 1,000 credits (immediate) Plan credits (after payment approval)
Sites Allowed 1 site 3-10 sites (depends on plan)
Payment Required No Yes (manual approval process)
Billing Form Not shown Required (8 fields)
Payment Method Not applicable User selects from country-specific options
Activation Time Immediate After admin approves payment

Payment Methods Available

The system supports 14 payment method configurations across different countries:

  • Active Methods: 6 (manual payment, bank transfers, local wallets)
  • Inactive Methods: 8 (Stripe and PayPal - disabled for manual payment workflow)

Global Payment Methods (Available Everywhere - country_code: *)

ACTIVE:

  1. Manual Payment - manual

    • Admin-assisted payment processing
    • Requires manual verification
    • Default fallback option
  2. Bank Transfer - bank_transfer

    • Manual bank transfer
    • Requires admin approval
    • Instructions: Bank details provided at checkout

INACTIVE (Disabled): 3. Credit/Debit Card (Stripe) - stripe DISABLED

  • Will be enabled when Stripe integration is complete
  1. PayPal - paypal DISABLED
    • Will be enabled when PayPal integration is complete

Pakistan-Specific Payment Methods (country_code: PK)

ACTIVE: 5. JazzCash / Easypaisa - local_wallet

  • Mobile wallet payments (JazzCash, Easypaisa)
  • Manual confirmation required
  • Instructions: "Send payment to JazzCash: 03001234567, Account Name: IGNY8"

India-Specific Payment Methods (country_code: IN)

ACTIVE: 6. Bank Transfer (NEFT/IMPS/RTGS) - bank_transfer

  • Indian bank transfer methods
  • Manual confirmation required
  1. UPI / Digital Wallet - local_wallet
    • UPI, Google Pay, PhonePe, Paytm
    • Manual confirmation required

INACTIVE (Disabled): 8. Stripe (India) - stripe DISABLED 9. PayPal (India) - paypal DISABLED

UK-Specific Payment Methods (country_code: GB)

ACTIVE: 10. Bank Transfer (BACS/Faster Payments) - bank_transfer - UK bank transfer methods - Manual confirmation required

INACTIVE (Disabled): 11. Stripe (UK) - stripe DISABLED 12. PayPal (UK) - paypal DISABLED

USA-Specific Payment Methods (country_code: US)

INACTIVE (Disabled): 13. Stripe (USA) - stripe DISABLED 14. PayPal (USA) - paypal DISABLED

Payment Method Selection Logic:

// Frontend fetches: GET /api/v1/billing/payment-methods/?country=PK
// Returns: Active global methods (*) + Active Pakistan-specific methods (PK)
// For Pakistan: 2 global + 1 Pakistan = 3 active methods

Current Implementation Status:

  • Manual payment methods (manual, bank_transfer, local_wallet) - ACTIVE & FULLY IMPLEMENTED
  • Automated methods (stripe, paypal) - DISABLED (will be enabled when integration is complete)

System Components

Backend:

  • Django 4.x with PostgreSQL
  • REST API (/api/v1/)
  • Celery for async tasks
  • Docker deployment

Frontend:

  • React 19 + TypeScript
  • Vite build system
  • Zustand state management
  • Tailwind CSS + TailAdmin template

Key Models:

  • User - Django authentication user
  • Account - Tenant account (one per organization)
  • Subscription - Tracks plan subscription
  • Plan - Available subscription plans
  • Site - WordPress sites managed by tenant
  • Invoice - Billing invoices
  • Payment - Payment records
  • PaymentMethodConfig - Country-specific payment methods (14 configurations)
  • AccountPaymentMethod - User's selected payment method
  • CreditTransaction - Credit allocation/deduction log

Payment System:

The platform uses a flexible, country-aware payment method system with manual payment workflow:

Payment Method Types:

  1. Manual Payments (Currently active):

    • manual - Admin-assisted manual payment processing
    • bank_transfer - Bank transfers (global + country-specific)
    • local_wallet - Mobile/digital wallets (JazzCash, Easypaisa, UPI)
    • Process: User pays → Submits confirmation → Admin approves
    • Status: FULLY IMPLEMENTED AND ACTIVE
  2. Automated Payments (Currently disabled):

    • stripe - Credit/Debit cards via Stripe
    • paypal - PayPal payments
    • Status: DISABLED (will be enabled when integration is complete)

Country-Specific Payment Methods:

Country Available Methods Total Active
Pakistan (PK) Manual, Bank Transfer (Global) + JazzCash/Easypaisa (Local) 3
India (IN) Manual, Bank Transfer (Global) + Bank Transfer (NEFT/IMPS), UPI (Local) 4
UK (GB) Manual, Bank Transfer (Global) + Bank Transfer (BACS/Faster) (Local) 3
USA (US) Manual, Bank Transfer (Global only) 2
Other Countries Manual, Bank Transfer (Global only) 2

Payment Workflow:

User selects country → System filters active payment methods → User chooses method →
User pays offline → Uploads proof → Submits confirmation → Admin approves → Account activated

Database Tables:

  • igny8_payment_method_config - 14 payment method configurations (6 active, 8 inactive)
  • igny8_account_payment_methods - User's selected payment method
  • igny8_payments - Payment transaction records
  • igny8_invoices - Billing invoices linked to payments

System Architecture

High-Level Architecture

┌─────────────────────────────────────────────────────────────┐
│                        FRONTEND                              │
│  ┌────────────┐  ┌─────────────┐  ┌──────────────────┐     │
│  │  SignUp    │  │   AppLayout │  │  Sites Dashboard │     │
│  │  (3 Steps) │  │  + Banner   │  │  (Create Sites)  │     │
│  └────────────┘  └─────────────┘  └──────────────────┘     │
└─────────────────────────────────────────────────────────────┘
                            ↓ HTTPS/REST API
┌─────────────────────────────────────────────────────────────┐
│                      DJANGO BACKEND                          │
│  ┌──────────────┐  ┌───────────────┐  ┌─────────────┐      │
│  │ Auth Views   │  │ Billing Views │  │ Site Views  │      │
│  │ (Register)   │  │ (Payments)    │  │ (CRUD)      │      │
│  └──────────────┘  └───────────────┘  └─────────────┘      │
│                            ↓                                 │
│  ┌──────────────┐  ┌───────────────┐  ┌─────────────┐      │
│  │ Serializers  │  │   Services    │  │   Admin     │      │
│  │ (Validation) │  │   (Business)  │  │  (Approval) │      │
│  └──────────────┘  └───────────────┘  └─────────────┘      │
└─────────────────────────────────────────────────────────────┘
                            ↓ ORM
┌─────────────────────────────────────────────────────────────┐
│                    POSTGRESQL DATABASE                       │
│  ┌──────┐ ┌─────────┐ ┌──────┐ ┌─────────┐ ┌──────────┐   │
│  │Users │ │Accounts │ │Plans │ │Invoices │ │Payments  │   │
│  └──────┘ └─────────┘ └──────┘ └─────────┘ └──────────┘   │
│  ┌──────────────┐ ┌───────┐ ┌────────────────────┐         │
│  │Subscriptions │ │Sites  │ │PaymentMethodConfig │         │
│  └──────────────┘ └───────┘ └────────────────────┘         │
└─────────────────────────────────────────────────────────────┘

Request Flow

User Browser → React App → API Call → Django View → Serializer Validation
                                         ↓
                                    Service Layer
                                         ↓
                                    Database ORM
                                         ↓
                                    PostgreSQL
                                         ↓
                                    Response ← JSON

Authentication Flow

1. User submits registration form
2. Backend creates User + Account + Subscription
3. JWT tokens generated (access + refresh)
4. Frontend stores tokens in localStorage
5. Subsequent requests include: Authorization: Bearer {access_token}
6. Backend validates token and attaches user to request

Flow A: Free Trial User Journey

Overview

Free trial users get immediate access to the platform with 1,000 credits and can create 1 site. No payment or billing information is required.

Step-by-Step Flow

STEP 1: User Visits Signup Page

URL: https://app.igny8.com/signup

Frontend Component: SignUpFormEnhanced.tsx (Step 1/3 only for free trial)

UI Elements:

  • Email input
  • Password input (with strength indicator)
  • Password confirmation input
  • First name input
  • Last name input
  • Plan selection dropdown (defaults to "Free Trial")
  • "Create Account" button

User Action:

User fills:
- Email: john@example.com
- Password: SecurePass123!
- Password Confirm: SecurePass123!
- First Name: John
- Last Name: Doe
- Plan: Free Trial (selected by default)

Frontend Validation:

  • Email format validation
  • Password strength check (min 8 chars, 1 uppercase, 1 number, 1 special)
  • Password match confirmation
  • All required fields filled

STEP 2: Form Submission

Action: User clicks "Create Account"

Frontend Code:

// File: frontend/src/components/auth/SignUpFormEnhanced.tsx

const handleSubmit = async (e: React.FormEvent) => {
  e.preventDefault();
  
  // Since plan is 'free', skip billing steps
  const payload = {
    email: accountData.email,
    password: accountData.password,
    password_confirm: accountData.passwordConfirm,
    first_name: accountData.firstName,
    last_name: accountData.lastName,
    plan_slug: 'free' // Default for free trial
  };
  
  try {
    const response = await fetch('/api/v1/auth/register/', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload)
    });
    
    if (response.ok) {
      const data = await response.json();
      // Store tokens
      localStorage.setItem('access_token', data.data.access);
      localStorage.setItem('refresh_token', data.data.refresh);
      // Redirect to dashboard
      navigate('/dashboard');
    }
  } catch (error) {
    console.error('Registration failed:', error);
  }
};

API Request:

POST /api/v1/auth/register/
Content-Type: application/json

{
  "email": "john@example.com",
  "password": "SecurePass123!",
  "password_confirm": "SecurePass123!",
  "first_name": "John",
  "last_name": "Doe",
  "plan_slug": "free"
}

STEP 3: Backend Processing

File: backend/igny8_core/auth/views.py

View: RegisterView.create()

Process Flow:

  1. Serializer Validation
# File: backend/igny8_core/auth/serializers.py

class RegisterSerializer(serializers.ModelSerializer):
    def validate(self, attrs):
        # Check password match
        if attrs['password'] != attrs['password_confirm']:
            raise ValidationError("Passwords don't match")
        
        # Check email uniqueness
        if User.objects.filter(email=attrs['email']).exists():
            raise ValidationError("Email already registered")
        
        return attrs
  1. User Creation
def create(self, validated_data):
    plan_slug = validated_data.pop('plan_slug', 'free')
    password = validated_data.pop('password')
    validated_data.pop('password_confirm')
    
    # Get plan
    try:
        plan = Plan.objects.get(slug=plan_slug)
    except Plan.DoesNotExist:
        raise ValidationError("Invalid plan")
    
    # Create user
    user = User.objects.create_user(
        username=validated_data['email'],
        email=validated_data['email'],
        password=password,
        first_name=validated_data['first_name'],
        last_name=validated_data['last_name']
    )
  1. Account Creation
    # Create account (tenant)
    account = Account.objects.create(
        name=f"{user.first_name} {user.last_name}'s Account",
        email=user.email,
        plan=plan,
        status='active',  # ✅ Immediate activation for free trial
        credits=0  # Will be allocated next
    )
    
    # Link user to account
    user.account = account
    user.save()
  1. Subscription Creation
    # Create subscription
    from datetime import datetime, timedelta
    
    subscription = Subscription.objects.create(
        account=account,
        plan=plan,  # ✅ Foreign key to Plan model
        status='active',
        current_period_start=datetime.now(),
        current_period_end=datetime.now() + timedelta(days=30),
        trial_start=datetime.now(),
        trial_end=datetime.now() + timedelta(days=30),
        metadata={'signup_source': 'web', 'plan_slug': 'free'}
    )
  1. Credit Allocation
    # Allocate free trial credits
    from igny8_core.business.billing.services.credit_service import CreditService
    
    is_free_trial = plan.slug == 'free'
    
    if is_free_trial:
        # Allocate 1,000 credits for free trial
        CreditService.allocate_credits(
            account=account,
            amount=plan.included_credits,  # 1000
            source='free_trial',
            description='Free trial credits allocation'
        )

Credit Service Code:

# File: backend/igny8_core/business/billing/services/credit_service.py

@staticmethod
def allocate_credits(account, amount, source, description=''):
    """Allocate credits and create transaction record"""
    from igny8_core.business.billing.models import CreditTransaction
    
    # Update account credits
    account.credits += amount
    account.save(update_fields=['credits'])
    
    # Create transaction record
    CreditTransaction.objects.create(
        account=account,
        amount=amount,
        transaction_type='credit',
        source=source,
        description=description,
        balance_after=account.credits
    )
  1. Generate JWT Tokens
    # Generate authentication tokens
    from rest_framework_simplejwt.tokens import RefreshToken
    
    refresh = RefreshToken.for_user(user)
    access = refresh.access_token
    
    return {
        'user': user,
        'account': account,
        'access': str(access),
        'refresh': str(refresh)
    }

STEP 4: API Response

Response Status: 201 Created

Response Body:

{
  "success": true,
  "message": "Account created successfully",
  "data": {
    "user": {
      "id": 1,
      "email": "john@example.com",
      "first_name": "John",
      "last_name": "Doe"
    },
    "account": {
      "id": 1,
      "name": "John Doe's Account",
      "email": "john@example.com",
      "status": "active",
      "credits": 1000,
      "plan": {
        "id": 1,
        "name": "Free Trial",
        "slug": "free",
        "price": 0,
        "included_credits": 1000,
        "max_sites": 1
      }
    },
    "subscription": {
      "id": 1,
      "status": "active",
      "current_period_start": "2024-12-09T00:00:00Z",
      "current_period_end": "2025-01-08T00:00:00Z"
    },
    "access": "eyJ0eXAiOiJKV1QiLCJhbGc...",
    "refresh": "eyJ0eXAiOiJKV1QiLCJhbGc..."
  }
}

STEP 5: Database State After Registration

Table: auth_user

id | username           | email              | first_name | last_name | is_active
---|--------------------|--------------------|------------|-----------|----------
1  | john@example.com   | john@example.com   | John       | Doe       | true

Table: igny8_tenants (accounts)

id | name                  | email              | status | credits | plan_id
---|-----------------------|--------------------|--------|---------|--------
1  | John Doe's Account    | john@example.com   | active | 1000    | 1

Table: igny8_plans

id | name        | slug | price | included_credits | max_sites | is_active
---|-------------|------|-------|------------------|-----------|----------
1  | Free Trial  | free | 0.00  | 1000             | 1         | true

Table: igny8_subscriptions

id | account_id | plan_id | status | current_period_start | current_period_end
---|------------|---------|--------|----------------------|-------------------
1  | 1          | 1       | active | 2024-12-09 00:00:00  | 2025-01-08 00:00:00

Table: igny8_credit_transactions

id | account_id | amount | transaction_type | source      | balance_after | created_at
---|------------|--------|------------------|-------------|---------------|------------------
1  | 1          | 1000   | credit           | free_trial  | 1000          | 2024-12-09 10:30:00

STEP 6: Frontend Redirect to Dashboard

Frontend Code:

// After successful registration
if (response.ok) {
  const data = await response.json();
  
  // Store tokens in localStorage
  localStorage.setItem('access_token', data.data.access);
  localStorage.setItem('refresh_token', data.data.refresh);
  
  // Update Zustand store
  useAuthStore.getState().setUser(data.data.user);
  useAuthStore.getState().setAccount(data.data.account);
  
  // Redirect to dashboard
  navigate('/dashboard');
}

URL: https://app.igny8.com/dashboard

Component: AppLayout.tsx with dashboard content

UI Elements:

  • Header with user menu
  • Sidebar navigation
  • Main content area showing "Sites" page
  • Credits display: "1,000 credits available"
  • "Create New Site" button

STEP 7: User Creates First Site

Action: User clicks "Create New Site" button

Frontend Component: SiteCreationModal.tsx

Form Fields:

  • Site Name (required)
  • Domain/URL (required)
  • Industry (required) NEW - Now required
  • Site Type (blog, business, ecommerce, etc.)

User Input:

Site Name: My Tech Blog
Domain: https://mytechblog.com
Industry: Technology (selected from dropdown)
Site Type: Blog

API Request:

POST /api/v1/auth/sites/
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc...
Content-Type: application/json

{
  "name": "My Tech Blog",
  "domain": "https://mytechblog.com",
  "industry": 1,
  "site_type": "blog"
}

STEP 8: Site Creation Backend Processing

File: backend/igny8_core/auth/views.py

View: SiteViewSet.create()

Process:

  1. Validate Industry Required
# File: backend/igny8_core/auth/serializers.py

class SiteSerializer(serializers.ModelSerializer):
    class Meta:
        model = Site
        fields = ['name', 'domain', 'industry', 'site_type']
    
    def validate_industry(self, value):
        if not value:
            raise ValidationError("Industry is required")
        return value
  1. Check Site Limit
# File: backend/igny8_core/auth/views.py

def create(self, request):
    account = request.user.account
    
    # Check if user can create more sites
    current_site_count = Site.objects.filter(account=account).count()
    
    if current_site_count >= account.plan.max_sites:
        return error_response(
            error=f"You've reached your plan limit of {account.plan.max_sites} site(s)",
            status_code=400,
            request=request
        )
  1. Create Site
    # Create site
    serializer = self.get_serializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    
    site = Site.objects.create(
        account=account,
        name=serializer.validated_data['name'],
        domain=serializer.validated_data['domain'],
        industry=serializer.validated_data['industry'],  # ✅ Required
        site_type=serializer.validated_data.get('site_type', 'blog'),
        status='active'
    )
  1. Create Site User Access
    # Automatically grant owner access
    from igny8_core.auth.models import SiteUserAccess
    
    SiteUserAccess.objects.create(
        site=site,
        user=request.user,
        role='owner',  # Full access
        can_manage_content=True,
        can_manage_users=True,
        can_manage_settings=True
    )
  1. Response
    return success_response(
        data=SiteSerializer(site).data,
        message='Site created successfully',
        status_code=201,
        request=request
    )

STEP 9: Database State After Site Creation

Table: igny8_sites

id | account_id | name          | domain                  | industry_id | site_type | status
---|------------|---------------|-------------------------|-------------|-----------|-------
1  | 1          | My Tech Blog  | https://mytechblog.com  | 1           | blog      | active

Table: igny8_industries

id | name        | slug       | description
---|-------------|------------|---------------------------
1  | Technology  | technology | Technology and IT industry
2  | Healthcare  | healthcare | Healthcare and medical
3  | Education   | education  | Education and training

Table: igny8_site_user_access

id | site_id | user_id | role  | can_manage_content | can_manage_users | can_manage_settings
---|---------|---------|-------|--------------------|-----------------|--------------------- 
1  | 1       | 1       | owner | true               | true            | true

STEP 10: User Accesses Site Dashboard

Frontend: User is redirected to site-specific dashboard

URL: https://app.igny8.com/sites/1/dashboard

Available Actions:

  • Manage content (posts, pages)
  • View analytics
  • Configure settings
  • Add team members (if plan allows)

Credit Usage:

  • Creating posts: -10 credits per post
  • Generating AI content: -50 credits per generation
  • Publishing: -5 credits per publish

Current State:

  • Credits: 1,000 (no deductions yet)
  • Sites: 1/1 (limit reached for free plan)
  • Subscription: Active until 2025-01-08

Flow A Summary

Timeline:

  1. T+0 seconds: User visits signup page
  2. T+30 seconds: User fills form and submits
  3. T+31 seconds: Backend creates User, Account, Subscription
  4. T+32 seconds: Credits allocated (1,000)
  5. T+33 seconds: JWT tokens generated and returned
  6. T+34 seconds: User redirected to dashboard
  7. T+2 minutes: User creates first site (with industry)
  8. T+2.5 minutes: Site created, user has full access

Total Duration: ~3 minutes from signup to first site creation

Database Records Created:

  • 1 User
  • 1 Account (status: active)
  • 1 Subscription (status: active, plan_id: 1)
  • 1 CreditTransaction (+1000 credits)
  • 1 Site (industry_id: required)
  • 1 SiteUserAccess (role: owner)

No Payment Required: User can immediately start using the platform


Flow B: Paid Plan User Journey

Overview

Paid plan users must complete a 3-step signup process, submit payment confirmation, and wait for admin approval before gaining full access. The flow includes billing information collection, payment method selection, manual payment submission, and admin approval workflow.

Step-by-Step Flow

STEP 1: User Visits Signup Page with Plan Selected

URL: https://app.igny8.com/signup?plan=starter

Frontend Component: SignUpFormEnhanced.tsx (3-step wizard)

Step 1 of 3: Account Information

UI Elements:

  • Progress indicator: "Step 1 of 3: Account Information"
  • Email input
  • Password input (with strength indicator)
  • Password confirmation input
  • First name input
  • Last name input
  • Plan selection dropdown (pre-selected: "Starter Plan - $5,000/month")
  • "Continue to Billing" button

User Action:

User fills:
- Email: sarah@company.com
- Password: SecurePass456!
- Password Confirm: SecurePass456!
- First Name: Sarah
- Last Name: Smith
- Plan: Starter Plan ($5,000/month) - pre-selected

Frontend Validation:

  • All validations from Flow A
  • Plan validation: Ensure plan is not 'free'

Frontend Code:

// File: frontend/src/components/auth/SignUpFormEnhanced.tsx

const [step, setStep] = useState<1 | 2 | 3>(1);
const [accountData, setAccountData] = useState({
  email: '',
  password: '',
  passwordConfirm: '',
  firstName: '',
  lastName: '',
  planSlug: 'starter' // From URL parameter
});

const handleStep1Next = () => {
  // Validate account fields
  if (!accountData.email || !accountData.password || !accountData.firstName || !accountData.lastName) {
    setError('All fields are required');
    return;
  }
  
  // For paid plans, move to billing step
  if (accountData.planSlug !== 'free') {
    setStep(2); // Go to billing form
  } else {
    // Free trial, submit directly
    handleSubmit();
  }
};

STEP 2: Billing Information Form

Step 2 of 3: Billing Information

Frontend Component: BillingFormStep.tsx

UI Elements:

  • Progress indicator: "Step 2 of 3: Billing Information"
  • Billing email input
  • Address line 1 input
  • Address line 2 input (optional)
  • City input
  • State/Province input
  • Postal/ZIP code input
  • Country dropdown (45+ countries)
  • Tax ID input (optional)
  • "Back" button
  • "Continue to Payment" button

User Action:

User fills:
- Billing Email: billing@company.com
- Address Line 1: 123 Business Avenue
- Address Line 2: Suite 400
- City: Lahore
- State: Punjab
- Postal Code: 54000
- Country: Pakistan (PK)
- Tax ID: PK-TAX-12345 (optional)

Frontend Code:

// File: frontend/src/components/billing/BillingFormStep.tsx

interface BillingFormData {
  billing_email: string;
  billing_address_line1: string;
  billing_address_line2: string;
  billing_city: string;
  billing_state: string;
  billing_postal_code: string;
  billing_country: string;
  tax_id: string;
}

export const BillingFormStep: React.FC<Props> = ({ data, onChange, onNext, onBack }) => {
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    
    // Validate required fields
    if (!data.billing_email || !data.billing_address_line1 || 
        !data.billing_city || !data.billing_country) {
      setError('Required fields missing');
      return;
    }
    
    // Move to payment method step
    onNext();
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <div className="space-y-4">
        <Input
          label="Billing Email"
          type="email"
          value={data.billing_email}
          onChange={(e) => onChange({...data, billing_email: e.target.value})}
        />
        
        <Input
          label="Address Line 1"
          value={data.billing_address_line1}
          onChange={(e) => onChange({...data, billing_address_line1: e.target.value})}
        />
        
        {/* ... other fields ... */}
        
        <SelectDropdown
          label="Country"
          options={countries}
          value={data.billing_country}
          onChange={(value) => onChange({...data, billing_country: value})}
        />
      </div>
      
      <div className="flex justify-between mt-6">
        <button type="button" onClick={onBack}>Back</button>
        <button type="submit">Continue to Payment</button>
      </div>
    </form>
  );
};

STEP 3: Payment Method Selection

Step 3 of 3: Payment Method

Frontend Component: PaymentMethodSelect.tsx

Process:

  1. Fetch Payment Methods for Country
// File: frontend/src/components/billing/PaymentMethodSelect.tsx

useEffect(() => {
  if (country) {
    fetchPaymentMethods(country);
  }
}, [country]);

const fetchPaymentMethods = async (countryCode: string) => {
  setLoading(true);
  try {
    const methods = await getPaymentMethodsByCountry(countryCode);
    setPaymentMethods(methods);
  } catch (error) {
    setError('Failed to load payment methods');
  } finally {
    setLoading(false);
  }
};

API Call:

GET /api/v1/billing/payment-methods/?country=PK

API Response:

{
  "success": true,
  "data": [
    {
      "payment_method": "manual",
      "display_name": "Manual Payment",
      "instructions": "Contact support to arrange payment",
      "country_code": "*",
      "is_enabled": true
    },
    {
      "payment_method": "bank_transfer",
      "display_name": "Bank Transfer",
      "instructions": "Bank Name: ABC Bank\nAccount Name: IGNY8 Inc\nAccount Number: PK12345678\nIBAN: PK36SCBL0000001123456702\n\nPlease transfer the exact invoice amount and keep the transaction reference.",
      "country_code": "*",
      "is_enabled": true
    },
    {
      "payment_method": "local_wallet",
      "display_name": "JazzCash / Easypaisa",
      "wallet_type": "JazzCash",
      "wallet_id": "03001234567",
      "instructions": "Send payment to:\nJazzCash: 03001234567\nAccount Name: IGNY8\n\nPlease keep the transaction ID and confirm payment after sending.",
      "country_code": "PK",
      "is_enabled": true
    }
  ]
}

Note: Stripe and PayPal methods are currently disabled (is_enabled: false) and will not be returned by the API until automated payment integration is completed.

Payment Method Database Configuration:

Based on the actual database (verified via terminal output), the system has:

  • Total Payment Methods: 14 configurations
  • Active Methods: 6 (manual payment workflow)
  • Inactive Methods: 8 (Stripe and PayPal - disabled)
  • By Country:
    • Global (*): 4 configs (2 active: manual, bank_transfer | 2 inactive: stripe, paypal)
    • Pakistan (PK): 1 active (local_wallet: JazzCash/Easypaisa)
    • India (IN): 4 configs (2 active: bank_transfer, local_wallet | 2 inactive: stripe, paypal)
    • UK (GB): 3 configs (1 active: bank_transfer | 2 inactive: stripe, paypal)
    • USA (US): 2 inactive (stripe, paypal)

Example Database Records:

-- PaymentMethodConfig table (Active methods only)
id | country_code | payment_method | display_name                    | is_enabled | instructions
---|--------------|----------------|---------------------------------|------------|-------------
11 | *            | manual         | Manual Payment                  | TRUE       | Contact support...
10 | *            | bank_transfer  | Bank Transfer                   | TRUE       | Bank: ABC Bank...
14 | PK           | local_wallet   | JazzCash / Easypaisa            | TRUE       | Send to: 03001234567
6  | IN           | local_wallet   | UPI / Digital Wallet            | TRUE       | UPI ID: igny8@upi
5  | IN           | bank_transfer  | Bank Transfer (NEFT/IMPS/RTGS)  | TRUE       | Bank details...
9  | GB           | bank_transfer  | Bank Transfer (BACS/Faster)     | TRUE       | Sort code: 12-34-56

-- Inactive methods (Stripe and PayPal - 8 configs disabled)
12 | *            | stripe         | Credit/Debit Card (Stripe)      | FALSE      | NULL
13 | *            | paypal         | PayPal                          | FALSE      | NULL
1  | US           | stripe         | Credit/Debit Card               | FALSE      | NULL
2  | US           | paypal         | PayPal                          | FALSE      | NULL
7  | GB           | stripe         | Credit/Debit Card               | FALSE      | NULL
8  | GB           | paypal         | PayPal                          | FALSE      | NULL
3  | IN           | stripe         | Credit/Debit Card               | FALSE      | NULL
4  | IN           | paypal         | PayPal                          | FALSE      | NULL

Payment Method Selection in UI:

For a user in Pakistan, the payment method dropdown shows:

○ Manual Payment                       [Global - ACTIVE]
○ Bank Transfer                        [Global - ACTIVE]
● JazzCash / Easypaisa                 [Pakistan - ACTIVE - Selected]

Total: 3 active payment methods

For a user in India, the payment method dropdown shows:

○ Manual Payment                       [Global - ACTIVE]
○ Bank Transfer                        [Global - ACTIVE]
○ Bank Transfer (NEFT/IMPS/RTGS)       [India - ACTIVE]
○ UPI / Digital Wallet                 [India - ACTIVE]

Total: 4 active payment methods

For a user in UK, the payment method dropdown shows:

○ Manual Payment                       [Global - ACTIVE]
○ Bank Transfer                        [Global - ACTIVE]
○ Bank Transfer (BACS/Faster)          [UK - ACTIVE]

Total: 3 active payment methods

For a user in USA, the payment method dropdown shows:

○ Manual Payment                       [Global - ACTIVE]
○ Bank Transfer                        [Global - ACTIVE]

Total: 2 active payment methods (only global methods)

For a user in Other Countries (e.g., Canada, Australia), only global methods appear:

○ Manual Payment                       [Global - ACTIVE]
○ Bank Transfer                        [Global - ACTIVE]

Total: 2 active payment methods

Note: Stripe and PayPal options are currently disabled and will not appear in any dropdown until the automated payment integration is completed and enabled.

  1. Display Payment Methods

UI Elements:

  • Radio button group with 4 options for Pakistan
  • Each option shows:
    • Payment method icon/logo
    • Display name
    • Instructions (if manual method)
  • Selected: "JazzCash / Easypaisa"
  • Instructions panel showing JazzCash details
  • "Complete Signup" button

User Selection:

Selected: JazzCash / Easypaisa
Instructions visible: "Send payment to: JazzCash: 03001234567..."

Frontend Code:

return (
  <div className="payment-methods">
    <h3>Select Payment Method</h3>
    
    {paymentMethods.map((method) => (
      <label key={method.payment_method} className="payment-option">
        <input
          type="radio"
          name="payment_method"
          value={method.payment_method}
          checked={selectedMethod === method.payment_method}
          onChange={() => setSelectedMethod(method.payment_method)}
        />
        <div className="method-info">
          <span className="method-name">{method.display_name}</span>
          {method.instructions && (
            <div className="instructions">
              <pre>{method.instructions}</pre>
            </div>
          )}
        </div>
      </label>
    ))}
    
    <button onClick={handleFinalSubmit}>Complete Signup</button>
  </div>
);

STEP 4: Final Submission to Backend

Action: User clicks "Complete Signup"

Frontend Code:

const handleFinalSubmit = async () => {
  // Combine all form data
  const payload = {
    // Step 1: Account data
    email: accountData.email,
    password: accountData.password,
    password_confirm: accountData.passwordConfirm,
    first_name: accountData.firstName,
    last_name: accountData.lastName,
    plan_slug: accountData.planSlug, // 'starter'
    
    // Step 2: Billing data
    billing_email: billingData.billing_email,
    billing_address_line1: billingData.billing_address_line1,
    billing_address_line2: billingData.billing_address_line2,
    billing_city: billingData.billing_city,
    billing_state: billingData.billing_state,
    billing_postal_code: billingData.billing_postal_code,
    billing_country: billingData.billing_country,
    tax_id: billingData.tax_id,
    
    // Step 3: Payment method
    payment_method: selectedPaymentMethod // 'local_wallet'
  };
  
  try {
    const response = await fetch('/api/v1/auth/register/', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload)
    });
    
    if (response.ok) {
      const data = await response.json();
      // Store tokens
      localStorage.setItem('access_token', data.data.access);
      localStorage.setItem('refresh_token', data.data.refresh);
      
      // Redirect to dashboard (with pending payment banner)
      navigate('/dashboard');
    }
  } catch (error) {
    console.error('Registration failed:', error);
  }
};

API Request:

POST /api/v1/auth/register/
Content-Type: application/json

{
  "email": "sarah@company.com",
  "password": "SecurePass456!",
  "password_confirm": "SecurePass456!",
  "first_name": "Sarah",
  "last_name": "Smith",
  "plan_slug": "starter",
  "billing_email": "billing@company.com",
  "billing_address_line1": "123 Business Avenue",
  "billing_address_line2": "Suite 400",
  "billing_city": "Lahore",
  "billing_state": "Punjab",
  "billing_postal_code": "54000",
  "billing_country": "PK",
  "tax_id": "PK-TAX-12345",
  "payment_method": "local_wallet"
}

STEP 5: Backend Processing for Paid Plan

File: backend/igny8_core/auth/serializers.py

RegisterSerializer.create() - Paid Plan Path:

  1. Extract and Validate Billing Fields
def create(self, validated_data):
    # Extract plan
    plan_slug = validated_data.pop('plan_slug', 'free')
    plan = Plan.objects.get(slug=plan_slug)
    
    # Extract billing fields (NEW)
    billing_email = validated_data.pop('billing_email', None)
    billing_address_line1 = validated_data.pop('billing_address_line1', None)
    billing_address_line2 = validated_data.pop('billing_address_line2', None)
    billing_city = validated_data.pop('billing_city', None)
    billing_state = validated_data.pop('billing_state', None)
    billing_postal_code = validated_data.pop('billing_postal_code', None)
    billing_country = validated_data.pop('billing_country', None)
    tax_id = validated_data.pop('tax_id', None)
    payment_method = validated_data.pop('payment_method', 'stripe')
    
    # Check if free trial or paid
    is_free_trial = plan.slug == 'free'
  1. Create User and Account
    # Create user (same as free trial)
    user = User.objects.create_user(
        username=validated_data['email'],
        email=validated_data['email'],
        password=validated_data.pop('password'),
        first_name=validated_data['first_name'],
        last_name=validated_data['last_name']
    )
    
    # Create account with DIFFERENT status
    account = Account.objects.create(
        name=f"{user.first_name} {user.last_name}'s Account",
        email=user.email,
        plan=plan,
        status='pending_payment' if not is_free_trial else 'active',  # ⚠️ Different
        credits=0,  # No credits until payment approved
        
        # Save billing information (NEW)
        billing_email=billing_email or user.email,
        billing_address_line1=billing_address_line1,
        billing_address_line2=billing_address_line2,
        billing_city=billing_city,
        billing_state=billing_state,
        billing_postal_code=billing_postal_code,
        billing_country=billing_country,
        tax_id=tax_id
    )
    
    user.account = account
    user.save()
  1. Create Subscription
    # Create subscription (same as free trial)
    subscription = Subscription.objects.create(
        account=account,
        plan=plan,
        status='active',  # Subscription is active, but account is pending
        current_period_start=datetime.now(),
        current_period_end=datetime.now() + timedelta(days=30)
    )
  1. Create AccountPaymentMethod
    # Create payment method record (NEW for paid plans)
    if not is_free_trial:
        from igny8_core.business.billing.models import AccountPaymentMethod
        
        AccountPaymentMethod.objects.create(
            account=account,
            type=payment_method,  # 'local_wallet'
            is_default=True,
            is_enabled=True,
            metadata={'selected_at_signup': True}
        )
  1. Create Invoice
    # Create invoice for paid plans
    if not is_free_trial:
        from igny8_core.business.billing.models import Invoice
        
        # Calculate invoice amount
        invoice_amount = plan.price  # $5,000 for starter plan
        
        invoice = Invoice.objects.create(
            account=account,
            subscription=subscription,
            amount_due=invoice_amount,
            subtotal=invoice_amount,
            tax=0,  # Can add tax calculation based on country
            total=invoice_amount,
            currency='USD',
            status='open',
            due_date=datetime.now() + timedelta(days=7),
            payment_method=payment_method,
            metadata={
                'billing_snapshot': {
                    'email': billing_email,
                    'address': {
                        'line1': billing_address_line1,
                        'line2': billing_address_line2,
                        'city': billing_city,
                        'state': billing_state,
                        'postal_code': billing_postal_code,
                        'country': billing_country
                    },
                    'tax_id': tax_id
                },
                'plan_details': {
                    'name': plan.name,
                    'slug': plan.slug,
                    'price': str(plan.price)
                }
            }
        )
  1. Generate Tokens and Prepare Response
    # Generate JWT tokens (same as free trial)
    from rest_framework_simplejwt.tokens import RefreshToken
    
    refresh = RefreshToken.for_user(user)
    access = refresh.access_token
    
    # Prepare response data
    response_data = {
        'user': UserSerializer(user).data,
        'account': AccountSerializer(account).data,
        'access': str(access),
        'refresh': str(refresh)
    }
    
    # Add invoice info for paid plans
    if not is_free_trial:
        # Get payment method config for instructions
        pm_config = PaymentMethodConfig.objects.filter(
            country_code=billing_country,
            payment_method=payment_method,
            is_enabled=True
        ).first()
        
        if not pm_config:
            pm_config = PaymentMethodConfig.objects.filter(
                country_code='*',
                payment_method=payment_method,
                is_enabled=True
            ).first()
        
        response_data['invoice'] = {
            'id': invoice.id,
            'amount': float(invoice.total),
            'currency': invoice.currency,
            'due_date': invoice.due_date.isoformat(),
            'status': invoice.status
        }
        
        response_data['payment_instructions'] = {
            'method': payment_method,
            'display_name': pm_config.display_name if pm_config else payment_method,
            'instructions': pm_config.instructions if pm_config else None,
            'wallet_id': pm_config.wallet_id if hasattr(pm_config, 'wallet_id') else None
        }
    
    return response_data

STEP 6: API Response for Paid Plan

Response Status: 201 Created

Response Body:

{
  "success": true,
  "message": "Account created. Please complete payment to activate your account.",
  "data": {
    "user": {
      "id": 2,
      "email": "sarah@company.com",
      "first_name": "Sarah",
      "last_name": "Smith"
    },
    "account": {
      "id": 2,
      "name": "Sarah Smith's Account",
      "email": "sarah@company.com",
      "status": "pending_payment",
      "credits": 0,
      "plan": {
        "id": 2,
        "name": "Starter Plan",
        "slug": "starter",
        "price": 5000,
        "included_credits": 5000,
        "max_sites": 3
      },
      "billing_email": "billing@company.com",
      "billing_country": "PK"
    },
    "subscription": {
      "id": 2,
      "status": "active",
      "current_period_start": "2024-12-09T00:00:00Z",
      "current_period_end": "2025-01-08T00:00:00Z"
    },
    "invoice": {
      "id": 1,
      "amount": 5000.00,
      "currency": "USD",
      "due_date": "2024-12-16T00:00:00Z",
      "status": "open"
    },
    "payment_instructions": {
      "method": "local_wallet",
      "display_name": "JazzCash / Easypaisa",
      "instructions": "Send payment to:\nJazzCash: 03001234567\nAccount Name: IGNY8\n\nPlease keep the transaction ID and confirm payment after sending.",
      "wallet_id": "03001234567"
    },
    "access": "eyJ0eXAiOiJKV1QiLCJhbGc...",
    "refresh": "eyJ0eXAiOiJKV1QiLCJhbGc..."
  }
}

Key Differences from Free Trial Response:

  • ⚠️ account.status = "pending_payment" (not "active")
  • ⚠️ account.credits = 0 (not 1000)
  • invoice object included
  • payment_instructions included

STEP 7: Database State After Paid Plan Registration

Table: auth_user

id | username              | email                 | first_name | last_name | is_active
---|----------------------|----------------------|------------|-----------|----------
2  | sarah@company.com     | sarah@company.com    | Sarah      | Smith     | true

Table: igny8_tenants (accounts)

id | name                | email              | status           | credits | plan_id | billing_email        | billing_country
---|---------------------|--------------------|--------------------|---------|---------|---------------------|----------------
2  | Sarah Smith's Acct  | sarah@company.com  | pending_payment   | 0       | 2       | billing@company.com | PK

Table: igny8_plans

id | name          | slug    | price    | included_credits | max_sites
---|---------------|---------|----------|------------------|----------
2  | Starter Plan  | starter | 5000.00  | 5000             | 3

Table: igny8_subscriptions

id | account_id | plan_id | status | current_period_start | current_period_end
---|------------|---------|--------|----------------------|-------------------
2  | 2          | 2       | active | 2024-12-09 00:00:00  | 2025-01-08 00:00:00

Table: igny8_account_payment_methods

id | account_id | type          | is_default | is_enabled | created_at
---|------------|---------------|------------|------------|------------------
1  | 2          | local_wallet  | true       | true       | 2024-12-09 11:00:00

Table: igny8_invoices

id | account_id | subscription_id | total    | currency | status | due_date            | payment_method
---|------------|-----------------|----------|----------|--------|---------------------|---------------
1  | 2          | 2               | 5000.00  | USD      | open   | 2024-12-16 00:00:00 | local_wallet

Table: igny8_credit_transactions

-- EMPTY (No credits allocated yet, waiting for payment approval)

Table: igny8_payments

-- EMPTY (User hasn't confirmed payment yet)

STEP 8: User Redirected to Dashboard with Pending Payment Banner

Frontend: User is redirected to dashboard but with limited access

URL: https://app.igny8.com/dashboard

Component: AppLayout.tsx with PendingPaymentBanner.tsx

UI State:

  • ⚠️ Banner at top: Large orange/yellow banner showing pending payment
  • Banner content:
    • "Payment Required"
    • Invoice details: $5,000 USD due by Dec 16, 2024
    • Payment method: JazzCash / Easypaisa
    • "Confirm Payment" button
  • Main dashboard: Grayed out / disabled
  • "Create Site" button: Disabled with tooltip "Complete payment to create sites"

Frontend Code:

// File: frontend/src/components/billing/PendingPaymentBanner.tsx

export const PendingPaymentBanner: React.FC = () => {
  const { user } = useAuthStore();
  const [invoice, setInvoice] = useState<Invoice | null>(null);
  const [showModal, setShowModal] = useState(false);
  
  useEffect(() => {
    // Fetch pending invoice if account status is pending_payment
    if (user?.account?.status === 'pending_payment') {
      fetchPendingInvoice();
    }
  }, [user]);
  
  const fetchPendingInvoice = async () => {
    const response = await fetch('/api/v1/billing/invoices/?status=open', {
      headers: { 'Authorization': `Bearer ${localStorage.getItem('access_token')}` }
    });
    const data = await response.json();
    if (data.success && data.data.length > 0) {
      setInvoice(data.data[0]);
    }
  };
  
  if (user?.account?.status !== 'pending_payment' || !invoice) {
    return null;
  }
  
  return (
    <div className="bg-yellow-100 border-l-4 border-yellow-500 p-4">
      <div className="flex items-center justify-between">
        <div>
          <h3 className="text-lg font-semibold text-yellow-800">
            Payment Required
          </h3>
          <p className="text-yellow-700">
            Invoice #{invoice.id} - ${invoice.total} {invoice.currency} due by {formatDate(invoice.due_date)}
          </p>
          <p className="text-sm text-yellow-600">
            Payment Method: {invoice.payment_method}
          </p>
        </div>
        <button
          onClick={() => setShowModal(true)}
          className="bg-yellow-600 text-white px-6 py-2 rounded hover:bg-yellow-700"
        >
          Confirm Payment
        </button>
      </div>
      
      {showModal && (
        <PaymentConfirmationModal
          invoice={invoice}
          onClose={() => setShowModal(false)}
          onSuccess={() => {
            setShowModal(false);
            // Refresh invoice status
            fetchPendingInvoice();
          }}
        />
      )}
    </div>
  );
};

Banner Appearance:

╔════════════════════════════════════════════════════════════════════╗
║  ⚠️  PAYMENT REQUIRED                                              ║
║  Invoice #1 - $5,000 USD due by Dec 16, 2024                      ║
║  Payment Method: JazzCash / Easypaisa                             ║
║                                          [Confirm Payment Button] ║
╚════════════════════════════════════════════════════════════════════╝

STEP 9: User Makes Payment and Clicks "Confirm Payment"

Action: User transfers money via JazzCash to 03001234567

User's JazzCash Transaction:

Transaction ID: JC-20241209-789456
Amount: 5,000 PKR (equivalent to $5,000 USD)
To: 03001234567 (IGNY8)
Date: Dec 9, 2024 11:30 AM
Status: Success

User clicks "Confirm Payment" button

Modal Opens: PaymentConfirmationModal.tsx

Modal UI Elements:

  • Title: "Confirm Your Payment"
  • Invoice details recap
  • Transaction reference input (required)
  • Notes textarea (optional)
  • File upload for proof (optional - JPEG/PNG/PDF, max 5MB)
  • "Submit Confirmation" button
  • "Cancel" button

User Input:

Transaction Reference: JC-20241209-789456
Notes: Transferred via JazzCash mobile app on Dec 9, 2024 at 11:30 AM
File: [Uploads screenshot of JazzCash receipt - receipt.png]

Frontend Code:

// File: frontend/src/components/billing/PaymentConfirmationModal.tsx

export const PaymentConfirmationModal: React.FC<Props> = ({ invoice, onClose, onSuccess }) => {
  const [reference, setReference] = useState('');
  const [notes, setNotes] = useState('');
  const [file, setFile] = useState<File | null>(null);
  const [uploading, setUploading] = useState(false);
  
  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const selectedFile = e.target.files?.[0];
    if (selectedFile) {
      // Validate file type
      const validTypes = ['image/jpeg', 'image/png', 'application/pdf'];
      if (!validTypes.includes(selectedFile.type)) {
        alert('Please upload JPEG, PNG, or PDF only');
        return;
      }
      
      // Validate file size (max 5MB)
      if (selectedFile.size > 5 * 1024 * 1024) {
        alert('File size must be less than 5MB');
        return;
      }
      
      setFile(selectedFile);
    }
  };
  
  const handleSubmit = async () => {
    if (!reference) {
      alert('Transaction reference is required');
      return;
    }
    
    setUploading(true);
    
    try {
      let proofUrl = null;
      
      // Upload file if provided
      if (file) {
        const formData = new FormData();
        formData.append('file', file);
        
        const uploadResponse = await fetch('/api/v1/media/upload/', {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${localStorage.getItem('access_token')}`
          },
          body: formData
        });
        
        if (uploadResponse.ok) {
          const uploadData = await uploadResponse.json();
          proofUrl = uploadData.data.url;
        }
      }
      
      // Submit payment confirmation
      const response = await confirmPayment({
        invoice_id: invoice.id,
        manual_reference: reference,
        manual_notes: notes,
        proof_url: proofUrl
      });
      
      if (response.success) {
        // Show success message
        alert('Payment confirmation submitted! Waiting for admin approval.');
        onSuccess();
      }
    } catch (error) {
      alert('Failed to submit confirmation. Please try again.');
    } finally {
      setUploading(false);
    }
  };
  
  return (
    <div className="modal">
      <div className="modal-content">
        <h2>Confirm Your Payment</h2>
        
        <div className="invoice-recap">
          <p>Invoice: #{invoice.id}</p>
          <p>Amount: ${invoice.total} {invoice.currency}</p>
          <p>Payment Method: {invoice.payment_method}</p>
        </div>
        
        <form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
          <div className="form-group">
            <label>Transaction Reference *</label>
            <input
              type="text"
              value={reference}
              onChange={(e) => setReference(e.target.value)}
              placeholder="e.g., JC-20241209-789456"
              required
            />
          </div>
          
          <div className="form-group">
            <label>Notes (Optional)</label>
            <textarea
              value={notes}
              onChange={(e) => setNotes(e.target.value)}
              placeholder="Additional information about the payment..."
              rows={3}
            />
          </div>
          
          <div className="form-group">
            <label>Upload Proof (Optional)</label>
            <input
              type="file"
              accept=".jpg,.jpeg,.png,.pdf"
              onChange={handleFileChange}
            />
            {file && <p className="file-name">Selected: {file.name}</p>}
          </div>
          
          <div className="modal-actions">
            <button type="button" onClick={onClose} disabled={uploading}>
              Cancel
            </button>
            <button type="submit" disabled={uploading || !reference}>
              {uploading ? 'Submitting...' : 'Submit Confirmation'}
            </button>
          </div>
        </form>
      </div>
    </div>
  );
};

STEP 10: Payment Confirmation API Call

API Request:

POST /api/v1/billing/payments/confirm/
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc...
Content-Type: application/json

{
  "invoice_id": 1,
  "manual_reference": "JC-20241209-789456",
  "manual_notes": "Transferred via JazzCash mobile app on Dec 9, 2024 at 11:30 AM",
  "proof_url": "https://igny8.s3.amazonaws.com/payment-proofs/receipt-123.png"
}

Backend Processing:

File: backend/igny8_core/billing/views.py

View: BillingViewSet.confirm_payment()

@action(detail=False, methods=['post'], url_path='payments/confirm')
def confirm_payment(self, request):
    """User confirms manual payment with reference"""
    invoice_id = request.data.get('invoice_id')
    manual_reference = request.data.get('manual_reference')
    manual_notes = request.data.get('manual_notes', '')
    proof_url = request.data.get('proof_url')
    
    # Validation
    if not invoice_id or not manual_reference:
        return error_response(
            error='invoice_id and manual_reference are required',
            status_code=400,
            request=request
        )
    
    try:
        # Get invoice
        invoice = Invoice.objects.get(
            id=invoice_id,
            account=request.account
        )
        
        # Check if payment already exists
        existing_payment = Payment.objects.filter(
            invoice=invoice,
            status__in=['pending_approval', 'completed']
        ).first()
        
        if existing_payment:
            return error_response(
                error='Payment already submitted for this invoice',
                status_code=400,
                request=request
            )
        
        # Create payment record
        payment = Payment.objects.create(
            account=request.account,
            invoice=invoice,
            amount=invoice.total,
            currency=invoice.currency,
            status='pending_approval',  # ⚠️ Requires admin approval
            payment_method=invoice.payment_method or 'bank_transfer',
            manual_reference=manual_reference,
            manual_notes=manual_notes,
            metadata={
                'proof_url': proof_url,
                'submitted_at': datetime.now().isoformat(),
                'ip_address': request.META.get('REMOTE_ADDR')
            } if proof_url else {}
        )
        
        # Send notification to admin (optional)
        # send_payment_confirmation_notification(payment)
        
        return success_response(
            data={
                'payment_id': payment.id,
                'status': 'pending_approval',
                'message': 'Your payment confirmation has been submitted and is awaiting admin approval.'
            },
            message='Payment confirmation submitted for review',
            request=request
        )
        
    except Invoice.DoesNotExist:
        return error_response(
            error='Invoice not found',
            status_code=404,
            request=request
        )

API Response:

{
  "success": true,
  "message": "Payment confirmation submitted for review",
  "data": {
    "payment_id": 1,
    "status": "pending_approval",
    "message": "Your payment confirmation has been submitted and is awaiting admin approval."
  }
}

STEP 11: Database State After Payment Confirmation

Table: igny8_payments (NEW RECORD)

id | account_id | invoice_id | amount   | currency | status            | payment_method | manual_reference   | created_at
---|------------|------------|----------|----------|------------------|----------------|-------------------|------------------
1  | 2          | 1          | 5000.00  | USD      | pending_approval | local_wallet   | JC-20241209-78945 | 2024-12-09 11:45:00

Table: igny8_invoices (UNCHANGED - still "open")

id | account_id | total    | status | payment_method
---|------------|----------|--------|---------------
1  | 2          | 5000.00  | open   | local_wallet

Table: igny8_tenants (UNCHANGED - still "pending_payment")

id | name                | status           | credits
---|---------------------|------------------|--------
2  | Sarah Smith's Acct  | pending_payment  | 0

User Dashboard State:

  • Banner updated: "Payment confirmation submitted. Awaiting admin approval."
  • Dashboard still disabled
  • Cannot create sites yet

STEP 12: Admin Receives Notification and Reviews Payment

Admin Access: Admin logs into Django admin panel

URL: https://app.igny8.com/admin/billing/payment/

Admin View:

Payment List:

┌─────┬──────────────────┬─────────┬────────────┬──────────────────┬─────────────┐
│ ID  │ Account          │ Amount  │ Status     │ Reference        │ Created     │
├─────┼──────────────────┼─────────┼────────────┼──────────────────┼─────────────┤
│ 1   │ Sarah Smith's... │ $5,000  │ ⏳ Pending │ JC-20241209-... │ Dec 9, 11:45│
└─────┴──────────────────┴─────────┴────────────┴──────────────────┴─────────────┘

Admin clicks on Payment #1 to view details:

Payment Detail View:

Payment #1

Account: Sarah Smith's Account (#2)
Invoice: #1
Amount: $5,000.00 USD
Status: pending_approval
Payment Method: local_wallet (JazzCash / Easypaisa)

Transaction Details:
  Reference: JC-20241209-789456
  Notes: Transferred via JazzCash mobile app on Dec 9, 2024 at 11:30 AM
  Proof: [View Screenshot] (receipt.png)
  
Submitted: Dec 9, 2024 at 11:45 AM
IP Address: 192.168.1.100

Actions:
  [Approve Payment]  [Reject Payment]

Admin Verification Process:

  1. Admin checks JazzCash account for incoming payment
  2. Verifies transaction ID: JC-20241209-789456
  3. Confirms amount: 5,000 PKR received
  4. Views uploaded screenshot for additional proof
  5. Decision: APPROVE

STEP 13: Admin Approves Payment

Option 1: Django Admin Bulk Action

Admin selects Payment #1 and chooses "Approve selected payments" from Actions dropdown

Backend Code:

# File: backend/igny8_core/billing/admin.py

@admin.register(Payment)
class PaymentAdmin(admin.ModelAdmin):
    list_display = ['id', 'account', 'amount', 'status', 'payment_method', 'created_at']
    list_filter = ['status', 'payment_method', 'created_at']
    search_fields = ['account__name', 'manual_reference', 'external_payment_id']
    actions = ['approve_payments', 'reject_payments']
    
    def approve_payments(self, request, queryset):
        """Approve selected payments (bulk action)"""
        count = 0
        errors = []
        
        for payment in queryset.filter(status='pending_approval'):
            try:
                # Use atomic transaction for data integrity
                with transaction.atomic():
                    # 1. Update payment status
                    payment.status = 'completed'
                    payment.metadata['approved_by'] = request.user.id
                    payment.metadata['approved_at'] = datetime.now().isoformat()
                    payment.save()
                    
                    # 2. Update invoice status
                    invoice = payment.invoice
                    invoice.status = 'paid'
                    invoice.paid_at = datetime.now()
                    invoice.save()
                    
                    # 3. Activate account
                    account = payment.account
                    account.status = 'active'  # ✅ ACTIVATE ACCOUNT
                    account.save()
                    
                    # 4. Allocate credits
                    from igny8_core.business.billing.services.credit_service import CreditService
                    
                    plan_credits = account.plan.included_credits  # 5000
                    CreditService.allocate_credits(
                        account=account,
                        amount=plan_credits,
                        source='plan_activation',
                        description=f'Credits from {account.plan.name} plan activation'
                    )
                    
                    # 5. Send notification to user (optional)
                    # send_account_activated_email(account)
                    
                    count += 1
                    
            except Exception as e:
                errors.append(f"Payment {payment.id}: {str(e)}")
        
        # Show success/error messages
        if count > 0:
            self.message_user(
                request,
                f"Successfully approved {count} payment(s)",
                level=messages.SUCCESS
            )
        
        if errors:
            self.message_user(
                request,
                f"Errors: {', '.join(errors)}",
                level=messages.ERROR
            )
    
    approve_payments.short_description = "✅ Approve selected payments"
    
    def reject_payments(self, request, queryset):
        """Reject selected payments"""
        count = queryset.filter(
            status='pending_approval'
        ).update(
            status='failed',
            metadata={'rejected_by': request.user.id, 'rejected_at': datetime.now().isoformat()}
        )
        
        self.message_user(
            request,
            f"Rejected {count} payment(s)",
            level=messages.WARNING
        )
    
    reject_payments.short_description = "❌ Reject selected payments"

Option 2: API Endpoint (for custom admin UI)

API Request:

POST /api/v1/billing/payments/1/approve/
Authorization: Bearer <admin_token>
Content-Type: application/json

{}

Backend View:

# File: backend/igny8_core/billing/views.py

@action(detail=True, methods=['post'], url_path='approve', permission_classes=[IsAdminUser])
def approve_payment(self, request, pk=None):
    """Admin approves payment and activates account"""
    payment = self.get_object()
    
    if payment.status != 'pending_approval':
        return error_response(
            error='Payment is not in pending_approval status',
            status_code=400,
            request=request
        )
    
    try:
        with transaction.atomic():
            # Update payment
            payment.status = 'completed'
            payment.metadata['approved_by'] = request.user.id
            payment.metadata['approved_at'] = datetime.now().isoformat()
            payment.save()
            
            # Update invoice
            invoice = payment.invoice
            invoice.status = 'paid'
            invoice.paid_at = datetime.now()
            invoice.save()
            
            # Activate account
            account = payment.account
            account.status = 'active'
            account.save()
            
            # Allocate credits
            from .services.credit_service import CreditService
            CreditService.allocate_credits(
                account=account,
                amount=account.plan.included_credits,
                source='plan_activation',
                description=f'Credits from {account.plan.name} plan'
            )
        
        return success_response(
            data={
                'payment_id': payment.id,
                'payment_status': payment.status,
                'account_status': account.status,
                'credits_allocated': account.credits
            },
            message='Payment approved and account activated',
            request=request
        )
    
    except Exception as e:
        return error_response(
            error=f'Failed to approve payment: {str(e)}',
            status_code=500,
            request=request
        )

Admin Success Message:

✅ Successfully approved 1 payment(s)
   - Payment #1: Account activated, 5000 credits allocated

STEP 14: Database State After Approval

Table: igny8_payments (UPDATED)

id | account_id | amount   | status    | manual_reference   | metadata
---|------------|----------|-----------|--------------------|---------------------------------
1  | 2          | 5000.00  | completed | JC-20241209-78945 | {"approved_by": 1, "approved_at": "2024-12-09T12:00:00"}

Table: igny8_invoices (UPDATED)

id | account_id | total    | status | paid_at
---|------------|----------|--------|------------------
1  | 2          | 5000.00  | paid   | 2024-12-09 12:00:00

Table: igny8_tenants (UPDATED - ACTIVATED)

id | name                | status  | credits
---|---------------------|---------|--------
2  | Sarah Smith's Acct  | active  | 5000

Table: igny8_credit_transactions (NEW RECORD)

id | account_id | amount | transaction_type | source           | balance_after | created_at
---|------------|--------|------------------|------------------|---------------|------------------
1  | 2          | 5000   | credit           | plan_activation  | 5000          | 2024-12-09 12:00:00

STEP 15: User Dashboard Updates Automatically

Frontend Polling or WebSocket: Frontend detects account status change

Polling Code (Option 1):

// File: frontend/src/hooks/useAccountStatus.ts

export const useAccountStatus = () => {
  const { user, setAccount } = useAuthStore();
  
  useEffect(() => {
    if (user?.account?.status === 'pending_payment') {
      // Poll every 30 seconds
      const interval = setInterval(async () => {
        const response = await fetch('/api/v1/auth/me/', {
          headers: { 'Authorization': `Bearer ${localStorage.getItem('access_token')}` }
        });
        
        if (response.ok) {
          const data = await response.json();
          if (data.data.account.status === 'active') {
            // Account activated!
            setAccount(data.data.account);
            clearInterval(interval);
            
            // Show success notification
            toast.success('Your account has been activated! 🎉');
          }
        }
      }, 30000); // 30 seconds
      
      return () => clearInterval(interval);
    }
  }, [user?.account?.status]);
};

Dashboard Changes:

Before Approval:

╔════════════════════════════════════════════════════════════╗
║  ⚠️  PAYMENT CONFIRMATION SUBMITTED                         ║
║  Awaiting admin approval...                                ║
╚════════════════════════════════════════════════════════════╝

Dashboard: [DISABLED - grayed out]
Create Site: [DISABLED]

After Approval:

╔════════════════════════════════════════════════════════════╗
║  ✅  ACCOUNT ACTIVATED!                                     ║
║  Your payment has been approved. 5,000 credits available.  ║
╚════════════════════════════════════════════════════════════╝

Dashboard: [ENABLED - full color]
Credits: 5,000
Sites: 0/3
[Create New Site] - ENABLED

Success Toast Notification:

🎉 Your account has been activated!
   You now have 5,000 credits and can create up to 3 sites.

STEP 16: User Creates First Site (Same as Flow A)

Action: User clicks "Create New Site"

Form Input:

Site Name: Company Blog
Domain: https://company.com/blog
Industry: Business Services (required)
Site Type: Blog

API Request:

POST /api/v1/auth/sites/
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc...
Content-Type: application/json

{
  "name": "Company Blog",
  "domain": "https://company.com/blog",
  "industry": 3,
  "site_type": "blog"
}

Backend Processing: (Same as Flow A Step 8)

Database State:

-- igny8_sites
id | account_id | name          | domain                    | industry_id | status
---|------------|---------------|---------------------------|-------------|-------
1  | 2          | Company Blog  | https://company.com/blog  | 3           | active

-- igny8_site_user_access
id | site_id | user_id | role  | can_manage_content
---|---------|---------|-------|-------------------
1  | 1       | 2       | owner | true

Success Response:

{
  "success": true,
  "message": "Site created successfully",
  "data": {
    "id": 1,
    "name": "Company Blog",
    "domain": "https://company.com/blog",
    "industry": {
      "id": 3,
      "name": "Business Services"
    },
    "status": "active"
  }
}

User Can Now:

  • Create up to 2 more sites (3 total)
  • Use 5,000 credits for content generation
  • Manage content across all sites
  • Add team members (if plan allows)

Flow B Summary

Timeline:

  1. T+0: User visits signup page with plan selected
  2. T+1 min: User fills Step 1 (account info)
  3. T+2 min: User fills Step 2 (billing info - 8 fields)
  4. T+3 min: User selects payment method (Step 3)
  5. T+4 min: Backend creates account (pending_payment), invoice
  6. T+5 min: User redirected to dashboard with banner
  7. T+10 min: User makes actual payment via JazzCash
  8. T+12 min: User submits payment confirmation
  9. T+15 min - 24 hrs: Admin reviews payment
  10. T+Admin approval: Admin approves → Account activated
  11. T+30 sec after: User dashboard updates automatically
  12. T+1 min after: User creates first site

Total Duration:

  • User actions: ~15 minutes
  • Admin approval: Variable (minutes to 24 hours)
  • Total: ~15 minutes to 24 hours from signup to first site

Database Records Created:

  • 1 User
  • 1 Account (status: pending_payment → active)
  • 1 Subscription (status: active)
  • 1 AccountPaymentMethod (type: local_wallet)
  • 1 Invoice (status: open → paid)
  • 1 Payment (status: pending_approval → completed)
  • 1 CreditTransaction (+5000 credits)
  • 1 Site (after activation)
  • 1 SiteUserAccess (role: owner)

Payment Required: Manual payment + admin approval workflow


Database Schema

Payment Method Configuration Details

Table: igny8_payment_method_config

This table stores all available payment methods with country-specific configurations.

CREATE TABLE igny8_payment_method_config (
    id                  SERIAL PRIMARY KEY,
    country_code        VARCHAR(2) NOT NULL,        -- '*' for global, 'PK', 'US', 'IN', 'GB', etc.
    payment_method      VARCHAR(50) NOT NULL,       -- 'manual', 'bank_transfer', 'local_wallet', 'stripe', 'paypal'
    display_name        VARCHAR(100) NOT NULL,      -- User-friendly name
    is_enabled          BOOLEAN DEFAULT TRUE,       -- TRUE for active, FALSE for disabled
    sort_order          INTEGER DEFAULT 0,
    instructions        TEXT,                       -- Payment instructions (for manual methods)
    wallet_type         VARCHAR(50),                -- 'JazzCash', 'Easypaisa', 'UPI', 'Zelle'
    wallet_id           VARCHAR(100),               -- Wallet account ID/number
    metadata            JSONB DEFAULT '{}',
    created_at          TIMESTAMP DEFAULT NOW(),
    updated_at          TIMESTAMP DEFAULT NOW()
);

-- Current active data (6 active methods)
INSERT INTO igny8_payment_method_config (id, country_code, payment_method, display_name, is_enabled, sort_order, instructions, wallet_type, wallet_id) VALUES
-- ACTIVE METHODS
(11, '*',  'manual',         'Manual Payment',                     TRUE, 1, 'Contact support to arrange payment', NULL, NULL),
(10, '*',  'bank_transfer',  'Bank Transfer',                      TRUE, 2, 'Bank: ABC Bank, Account: 123456...', NULL, NULL),
(14, 'PK', 'local_wallet',   'JazzCash / Easypaisa',              TRUE, 1, 'Send to: 03001234567', 'JazzCash', '03001234567'),
(5,  'IN', 'bank_transfer',  'Bank Transfer (NEFT/IMPS/RTGS)',    TRUE, 1, 'Bank details for India...', NULL, NULL),
(6,  'IN', 'local_wallet',   'UPI / Digital Wallet',              TRUE, 2, 'UPI ID: igny8@upi', 'UPI', 'igny8@upi'),
(9,  'GB', 'bank_transfer',  'Bank Transfer (BACS/Faster)',       TRUE, 1, 'Sort code: 12-34-56...', NULL, NULL),

-- INACTIVE METHODS (Disabled until automated payment integration)
(12, '*',  'stripe',         'Credit/Debit Card (Stripe)',        FALSE, 10, NULL, NULL, NULL),
(13, '*',  'paypal',         'PayPal',                            FALSE, 11, NULL, NULL, NULL),
(1,  'US', 'stripe',         'Credit/Debit Card',                 FALSE, 10, NULL, NULL, NULL),
(2,  'US', 'paypal',         'PayPal',                            FALSE, 11, NULL, NULL, NULL),
(7,  'GB', 'stripe',         'Credit/Debit Card',                 FALSE, 10, NULL, NULL, NULL),
(8,  'GB', 'paypal',         'PayPal',                            FALSE, 11, NULL, NULL, NULL),
(3,  'IN', 'stripe',         'Credit/Debit Card',                 FALSE, 10, NULL, NULL, NULL),
(4,  'IN', 'paypal',         'PayPal',                            FALSE, 11, NULL, NULL, NULL);

Key Fields:

  • country_code: ISO 2-letter country code or '*' for global availability
  • payment_method: Type of payment (manual, bank_transfer, local_wallet, stripe, paypal)
  • display_name: What users see in the dropdown
  • is_enabled: TRUE for active methods, FALSE for disabled (Stripe/PayPal are FALSE)
  • instructions: Detailed payment instructions for manual methods
  • wallet_id: Mobile wallet number or email for digital wallets
  • sort_order: Display order in dropdown (lower = higher priority)

Query Examples:

-- Get ACTIVE payment methods for Pakistan users
SELECT * FROM igny8_payment_method_config
WHERE (country_code = 'PK' OR country_code = '*')
  AND is_enabled = TRUE
ORDER BY sort_order;
-- Returns: 3 methods (manual, bank_transfer, local_wallet)

-- Get ACTIVE payment methods for India users
SELECT * FROM igny8_payment_method_config
WHERE (country_code = 'IN' OR country_code = '*')
  AND is_enabled = TRUE
ORDER BY sort_order;
-- Returns: 4 methods (manual, bank_transfer x2, local_wallet)

-- Get ACTIVE payment methods for UK users
SELECT * FROM igny8_payment_method_config
WHERE (country_code = 'GB' OR country_code = '*')
  AND is_enabled = TRUE
ORDER BY sort_order;
-- Returns: 3 methods (manual, bank_transfer x2)

-- Get ACTIVE payment methods for USA or other countries
SELECT * FROM igny8_payment_method_config
WHERE country_code = '*'
  AND is_enabled = TRUE
ORDER BY sort_order;
-- Returns: 2 methods (manual, bank_transfer - global only)

-- Count all methods by status
SELECT is_enabled, COUNT(*) as count
FROM igny8_payment_method_config
GROUP BY is_enabled;
-- Returns: TRUE: 6, FALSE: 8

Entity Relationship Diagram

┌──────────────┐         ┌──────────────┐         ┌──────────────┐
│    User      │─────────│   Account    │─────────│     Plan     │
│ (auth_user)  │  1:1    │ (tenants)    │  N:1    │  (plans)     │
└──────────────┘         └──────────────┘         └──────────────┘
                               │ 1:N                      │
                               │                          │
                         ┌─────┴─────┐                   │
                         │            │                   │
                         ▼            ▼                   ▼
                  ┌─────────┐  ┌──────────┐      ┌──────────────┐
                  │  Site   │  │Subscrip- │──────│ Subscription │
                  │(sites)  │  │  tion    │ N:1  │              │
                  └─────────┘  └──────────┘      └──────────────┘
                         │                              │ 1:N
                         │                              │
                         ▼                              ▼
                  ┌──────────────┐              ┌─────────────┐
                  │SiteUserAccess│              │   Invoice   │
                  │              │              │             │
                  └──────────────┘              └─────────────┘
                                                       │ 1:N
                                                       │
                                                       ▼
                                                 ┌─────────────┐
                                                 │   Payment   │
                                                 │             │
                                                 └─────────────┘

Key Tables and Fields

1. auth_user

id              SERIAL PRIMARY KEY
username        VARCHAR(150) UNIQUE NOT NULL
email           VARCHAR(254) UNIQUE NOT NULL
password        VARCHAR(128) NOT NULL
first_name      VARCHAR(150)
last_name       VARCHAR(150)
is_active       BOOLEAN DEFAULT TRUE
date_joined     TIMESTAMP DEFAULT NOW()

2. igny8_tenants (Account)

id                      SERIAL PRIMARY KEY
name                    VARCHAR(255) NOT NULL
email                   VARCHAR(254) NOT NULL
status                  VARCHAR(20) DEFAULT 'pending_payment'
                        -- Values: 'pending_payment', 'active', 'suspended', 'cancelled'
credits                 INTEGER DEFAULT 0
plan_id                 INTEGER REFERENCES igny8_plans(id) ON DELETE PROTECT
billing_email           VARCHAR(254)
billing_address_line1   VARCHAR(255)
billing_address_line2   VARCHAR(255)
billing_city            VARCHAR(100)
billing_state           VARCHAR(100)
billing_postal_code     VARCHAR(20)
billing_country         VARCHAR(2)
tax_id                  VARCHAR(50)
created_at              TIMESTAMP DEFAULT NOW()
updated_at              TIMESTAMP DEFAULT NOW()

3. igny8_plans

id                  SERIAL PRIMARY KEY
name                VARCHAR(100) NOT NULL
slug                VARCHAR(50) UNIQUE NOT NULL
price               DECIMAL(10,2) NOT NULL
billing_period      VARCHAR(20) DEFAULT 'monthly'
included_credits    INTEGER DEFAULT 0
max_sites           INTEGER DEFAULT 1
is_active           BOOLEAN DEFAULT TRUE

4. igny8_subscriptions

id                      SERIAL PRIMARY KEY
account_id              INTEGER REFERENCES igny8_tenants(id) ON DELETE CASCADE
plan_id                 INTEGER REFERENCES igny8_plans(id) ON DELETE PROTECT
status                  VARCHAR(20) DEFAULT 'active'
                        -- Values: 'active', 'cancelled', 'past_due'
current_period_start    TIMESTAMP
current_period_end      TIMESTAMP
trial_start             TIMESTAMP
trial_end               TIMESTAMP
metadata                JSONB DEFAULT '{}'
created_at              TIMESTAMP DEFAULT NOW()

5. igny8_invoices

id                  SERIAL PRIMARY KEY
account_id          INTEGER REFERENCES igny8_tenants(id) ON DELETE CASCADE
subscription_id     INTEGER REFERENCES igny8_subscriptions(id)
amount_due          DECIMAL(10,2) NOT NULL
subtotal            DECIMAL(10,2) NOT NULL
tax                 DECIMAL(10,2) DEFAULT 0
total               DECIMAL(10,2) NOT NULL
currency            VARCHAR(3) DEFAULT 'USD'
status              VARCHAR(20) DEFAULT 'open'
                    -- Values: 'draft', 'open', 'paid', 'void', 'uncollectible'
due_date            TIMESTAMP
paid_at             TIMESTAMP
payment_method      VARCHAR(50)
metadata            JSONB DEFAULT '{}'
created_at          TIMESTAMP DEFAULT NOW()

6. igny8_payments

id                      SERIAL PRIMARY KEY
account_id              INTEGER REFERENCES igny8_tenants(id) ON DELETE CASCADE
invoice_id              INTEGER REFERENCES igny8_invoices(id)
amount                  DECIMAL(10,2) NOT NULL
currency                VARCHAR(3) DEFAULT 'USD'
status                  VARCHAR(20) DEFAULT 'pending_approval'
                        -- Values: 'pending_approval', 'completed', 'failed'
payment_method          VARCHAR(50) NOT NULL
external_payment_id     VARCHAR(255)
manual_reference        VARCHAR(255)
manual_notes            TEXT
metadata                JSONB DEFAULT '{}'
created_at              TIMESTAMP DEFAULT NOW()

7. igny8_sites

id              SERIAL PRIMARY KEY
account_id      INTEGER REFERENCES igny8_tenants(id) ON DELETE CASCADE
name            VARCHAR(255) NOT NULL
domain          VARCHAR(255) NOT NULL
industry_id     INTEGER REFERENCES igny8_industries(id) ON DELETE PROTECT NOT NULL
site_type       VARCHAR(50) DEFAULT 'blog'
status          VARCHAR(20) DEFAULT 'active'
created_at      TIMESTAMP DEFAULT NOW()

8. igny8_credit_transactions

id                  SERIAL PRIMARY KEY
account_id          INTEGER REFERENCES igny8_tenants(id) ON DELETE CASCADE
amount              INTEGER NOT NULL
transaction_type    VARCHAR(20) NOT NULL
                    -- Values: 'credit', 'debit'
source              VARCHAR(50) NOT NULL
                    -- Values: 'free_trial', 'plan_activation', 'usage', 'refund'
description         TEXT
balance_after       INTEGER NOT NULL
created_at          TIMESTAMP DEFAULT NOW()

API Reference

Authentication Endpoints

1. Register User

POST /api/v1/auth/register/
Content-Type: application/json

Request Body:
{
  "email": "user@example.com",
  "password": "SecurePass123!",
  "password_confirm": "SecurePass123!",
  "first_name": "John",
  "last_name": "Doe",
  "plan_slug": "free" | "starter" | "professional" | "enterprise",
  
  // Required for paid plans only:
  "billing_email": "billing@company.com",
  "billing_address_line1": "123 Main St",
  "billing_address_line2": "Suite 400",
  "billing_city": "Lahore",
  "billing_state": "Punjab",
  "billing_postal_code": "54000",
  "billing_country": "PK",
  "tax_id": "PK-TAX-12345",
  "payment_method": "stripe" | "paypal" | "bank_transfer" | "local_wallet"
}

Response (201 Created):
{
  "success": true,
  "message": "Account created successfully",
  "data": {
    "user": { ... },
    "account": {
      "status": "active" | "pending_payment",
      "credits": 1000 | 0
    },
    "access": "JWT_ACCESS_TOKEN",
    "refresh": "JWT_REFRESH_TOKEN",
    
    // For paid plans only:
    "invoice": { ... },
    "payment_instructions": { ... }
  }
}

2. Login

POST /api/v1/auth/login/
Content-Type: application/json

{
  "email": "user@example.com",
  "password": "SecurePass123!"
}

Response (200 OK):
{
  "success": true,
  "data": {
    "access": "JWT_ACCESS_TOKEN",
    "refresh": "JWT_REFRESH_TOKEN",
    "user": { ... },
    "account": { ... }
  }
}

Billing Endpoints

3. Get Payment Methods by Country

GET /api/v1/billing/payment-methods/?country=PK
Authorization: Bearer {access_token}

Response (200 OK):
{
  "success": true,
  "data": [
    {
      "payment_method": "stripe",
      "display_name": "Credit/Debit Card (Stripe)",
      "country_code": "*",
      "instructions": null
    },
    {
      "payment_method": "local_wallet",
      "display_name": "JazzCash / Easypaisa",
      "country_code": "PK",
      "wallet_id": "03001234567",
      "instructions": "Send payment to:\nJazzCash: 03001234567..."
    }
  ]
}

4. Confirm Manual Payment

POST /api/v1/billing/payments/confirm/
Authorization: Bearer {access_token}
Content-Type: application/json

{
  "invoice_id": 1,
  "manual_reference": "JC-20241209-789456",
  "manual_notes": "Transferred via JazzCash on Dec 9",
  "proof_url": "https://s3.amazonaws.com/receipts/123.png"
}

Response (200 OK):
{
  "success": true,
  "message": "Payment confirmation submitted for review",
  "data": {
    "payment_id": 1,
    "status": "pending_approval"
  }
}

5. Approve Payment (Admin Only)

POST /api/v1/billing/payments/{payment_id}/approve/
Authorization: Bearer {admin_access_token}

Response (200 OK):
{
  "success": true,
  "message": "Payment approved and account activated",
  "data": {
    "payment_id": 1,
    "payment_status": "completed",
    "account_status": "active",
    "credits_allocated": 5000
  }
}

6. Reject Payment (Admin Only)

POST /api/v1/billing/payments/{payment_id}/reject/
Authorization: Bearer {admin_access_token}
Content-Type: application/json

{
  "reason": "Insufficient proof of payment"
}

Response (200 OK):
{
  "success": true,
  "message": "Payment rejected",
  "data": {
    "payment_id": 1,
    "status": "failed"
  }
}

Site Management Endpoints

7. Create Site

POST /api/v1/auth/sites/
Authorization: Bearer {access_token}
Content-Type: application/json

{
  "name": "My Tech Blog",
  "domain": "https://mytechblog.com",
  "industry": 1,
  "site_type": "blog"
}

Response (201 Created):
{
  "success": true,
  "message": "Site created successfully",
  "data": {
    "id": 1,
    "name": "My Tech Blog",
    "domain": "https://mytechblog.com",
    "industry": {
      "id": 1,
      "name": "Technology"
    },
    "status": "active"
  }
}

Error Response (400 Bad Request):
{
  "success": false,
  "error": "You've reached your plan limit of 1 site(s)"
}

8. List User Sites

GET /api/v1/auth/sites/
Authorization: Bearer {access_token}

Response (200 OK):
{
  "success": true,
  "data": [
    {
      "id": 1,
      "name": "My Tech Blog",
      "domain": "https://mytechblog.com",
      "industry": { ... },
      "status": "active",
      "created_at": "2024-12-09T12:30:00Z"
    }
  ]
}

State Transitions

Account Status State Machine

┌──────────────────┐
│  [Registration]  │
└────────┬─────────┘
         │
         ├─── Free Trial ───────────────┐
         │                               │
         │                               ▼
         │                        ┌─────────────┐
         │                        │   ACTIVE    │◄────┐
         │                        │ (immediate) │     │
         │                        └─────────────┘     │
         │                                            │
         │                                            │
         └─── Paid Plan ───────────┐                 │
                                    │                 │
                                    ▼                 │
                          ┌──────────────────┐        │
                          │ PENDING_PAYMENT  │        │
                          │ (awaiting payment)│       │
                          └────────┬─────────┘        │
                                   │                  │
                                   │ User confirms    │
                                   │ payment          │
                                   ▼                  │
                          ┌──────────────────┐        │
                          │ PENDING_APPROVAL │        │
                          │ (payment record  │        │
                          │  created)        │        │
                          └────────┬─────────┘        │
                                   │                  │
                                   │ Admin approves   │
                                   └──────────────────┘

Payment Status State Machine

┌──────────────────┐
│ User confirms    │
│ payment          │
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│ PENDING_APPROVAL │
│ (waiting admin)  │
└────────┬─────────┘
         │
         ├─── Admin Approves ────────────┐
         │                                │
         │                                ▼
         │                         ┌────────────┐
         │                         │ COMPLETED  │
         │                         │ (success)  │
         │                         └────────────┘
         │
         └─── Admin Rejects ─────────────┐
                                          │
                                          ▼
                                   ┌────────────┐
                                   │   FAILED   │
                                   │ (rejected) │
                                   └────────────┘

Invoice Status State Machine

┌──────────────┐
│ Account      │
│ created      │
│ (paid plan)  │
└──────┬───────┘
       │
       ▼
┌──────────┐
│   OPEN   │
│ (unpaid) │
└─────┬────┘
      │
      │ Payment approved
      │
      ▼
┌──────────┐
│   PAID   │
│ (success)│
└──────────┘

Error Handling

Common Error Scenarios

1. Email Already Registered

POST /api/v1/auth/register/
Response (400 Bad Request):
{
  "success": false,
  "error": "Email already registered",
  "error_code": "EMAIL_EXISTS"
}

2. Password Mismatch

POST /api/v1/auth/register/
Response (400 Bad Request):
{
  "success": false,
  "error": "Passwords don't match",
  "error_code": "PASSWORD_MISMATCH"
}

3. Invalid Plan

POST /api/v1/auth/register/
Response (400 Bad Request):
{
  "success": false,
  "error": "Invalid plan slug",
  "error_code": "INVALID_PLAN"
}

4. Site Limit Reached

POST /api/v1/auth/sites/
Response (400 Bad Request):
{
  "success": false,
  "error": "You've reached your plan limit of 1 site(s)",
  "error_code": "SITE_LIMIT_REACHED"
}

5. Industry Required

POST /api/v1/auth/sites/
Response (400 Bad Request):
{
  "success": false,
  "error": "Industry is required",
  "error_code": "INDUSTRY_REQUIRED"
}

6. Payment Already Submitted

POST /api/v1/billing/payments/confirm/
Response (400 Bad Request):
{
  "success": false,
  "error": "Payment already submitted for this invoice",
  "error_code": "PAYMENT_EXISTS"
}

7. Account Not Activated

POST /api/v1/auth/sites/
Response (403 Forbidden):
{
  "success": false,
  "error": "Account is not activated. Please complete payment.",
  "error_code": "ACCOUNT_NOT_ACTIVE"
}

Frontend Error Handling

Error Display Component:

const ErrorMessage: React.FC<{error: string}> = ({ error }) => {
  const errorMessages = {
    'EMAIL_EXISTS': 'This email is already registered. Please login instead.',
    'PASSWORD_MISMATCH': 'Passwords do not match. Please check and try again.',
    'SITE_LIMIT_REACHED': 'You have reached your plan limit. Upgrade to create more sites.',
    'ACCOUNT_NOT_ACTIVE': 'Please complete your payment to access this feature.',
    'INDUSTRY_REQUIRED': 'Please select an industry for your site.'
  };
  
  return (
    <div className="error-banner">
      {errorMessages[error] || error}
    </div>
  );
};

Testing Scenarios

Test Case 1: Free Trial Signup Flow

✓ Visit /signup
✓ Fill email: test@example.com
✓ Fill password: Test123!
✓ Fill name: Test User
✓ Select plan: Free Trial
✓ Submit form
✓ Verify redirect to /dashboard
✓ Verify account.status = 'active'
✓ Verify account.credits = 1000
✓ Create site with industry
✓ Verify site created successfully

Test Case 2: Paid Plan Signup → Approval Flow

✓ Visit /signup?plan=starter
✓ Step 1: Fill account info
✓ Step 2: Fill billing info (8 fields)
✓ Step 3: Select payment method (JazzCash)
✓ Submit all forms
✓ Verify account.status = 'pending_payment'
✓ Verify invoice created
✓ Verify banner shows "Payment Required"
✓ Make actual payment via JazzCash
✓ Click "Confirm Payment"
✓ Fill transaction reference
✓ Submit confirmation
✓ Verify payment.status = 'pending_approval'
✓ Admin approves payment
✓ Verify account.status = 'active'
✓ Verify credits allocated (5000)
✓ Create site
✓ Verify site created successfully

Test Case 3: Payment Rejection Flow

✓ Complete steps 1-12 from Test Case 2
✓ Admin rejects payment (instead of approving)
✓ Verify payment.status = 'failed'
✓ Verify account.status remains 'pending_payment'
✓ Verify user sees error message
✓ User can resubmit payment

Document Complete
Total Sections: 9
Total Lines: ~3,500
Coverage: 100% of both user journeys