diff --git a/COMPLETE-TENANCY-FLOW-DOCUMENTATION.md b/COMPLETE-TENANCY-FLOW-DOCUMENTATION.md new file mode 100644 index 00000000..03a57852 --- /dev/null +++ b/COMPLETE-TENANCY-FLOW-DOCUMENTATION.md @@ -0,0 +1,3151 @@ +# 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](#overview) +2. [System Architecture](#system-architecture) +3. [Flow A: Free Trial User Journey](#flow-a-free-trial-user-journey) +4. [Flow B: Paid Plan User Journey](#flow-b-paid-plan-user-journey) +5. [Database Schema](#database-schema) +6. [API Reference](#api-reference) +7. [State Transitions](#state-transitions) +8. [Error Handling](#error-handling) +9. [Testing Scenarios](#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 + +4. ~~**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 + +7. **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:** +```typescript +// 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:** +```typescript +// 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:** +```http +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** +```python +# 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 +``` + +2. **User Creation** +```python +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'] + ) +``` + +3. **Account Creation** +```python + # 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() +``` + +4. **Subscription Creation** +```python + # 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'} + ) +``` + +5. **Credit Allocation** +```python + # 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:** +```python +# 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 + ) +``` + +6. **Generate JWT Tokens** +```python + # 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:** +```json +{ + "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** +```sql +id | username | email | first_name | last_name | is_active +---|--------------------|--------------------|------------|-----------|---------- +1 | john@example.com | john@example.com | John | Doe | true +``` + +**Table: igny8_tenants (accounts)** +```sql +id | name | email | status | credits | plan_id +---|-----------------------|--------------------|--------|---------|-------- +1 | John Doe's Account | john@example.com | active | 1000 | 1 +``` + +**Table: igny8_plans** +```sql +id | name | slug | price | included_credits | max_sites | is_active +---|-------------|------|-------|------------------|-----------|---------- +1 | Free Trial | free | 0.00 | 1000 | 1 | true +``` + +**Table: igny8_subscriptions** +```sql +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** +```sql +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:** +```typescript +// 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:** +```http +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** +```python +# 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 +``` + +2. **Check Site Limit** +```python +# 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 + ) +``` + +3. **Create Site** +```python + # 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' + ) +``` + +4. **Create Site User Access** +```python + # 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 + ) +``` + +5. **Response** +```python + 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** +```sql +id | account_id | name | domain | industry_id | site_type | status +---|------------|---------------|-------------------------|-------------|-----------|------- +1 | 1 | My Tech Blog | https://mytechblog.com | 1 | blog | active +``` + +**Table: igny8_industries** +```sql +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** +```sql +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:** +```typescript +// 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:** +```typescript +// 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 = ({ 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 ( +
+
+ onChange({...data, billing_email: e.target.value})} + /> + + onChange({...data, billing_address_line1: e.target.value})} + /> + + {/* ... other fields ... */} + + onChange({...data, billing_country: value})} + /> +
+ +
+ + +
+
+ ); +}; +``` + +--- + +#### STEP 3: Payment Method Selection + +**Step 3 of 3: Payment Method** + +**Frontend Component:** `PaymentMethodSelect.tsx` + +**Process:** + +1. **Fetch Payment Methods for Country** +```typescript +// 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:** +```http +GET /api/v1/billing/payment-methods/?country=PK +``` + +**API Response:** +```json +{ + "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:** +```sql +-- 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. + +2. **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:** +```typescript +return ( +
+

Select Payment Method

+ + {paymentMethods.map((method) => ( + + ))} + + +
+); +``` + +--- + +#### STEP 4: Final Submission to Backend + +**Action:** User clicks "Complete Signup" + +**Frontend Code:** +```typescript +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:** +```http +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** +```python +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' +``` + +2. **Create User and Account** +```python + # 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() +``` + +3. **Create Subscription** +```python + # 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) + ) +``` + +4. **Create AccountPaymentMethod** +```python + # 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} + ) +``` + +5. **Create Invoice** +```python + # 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) + } + } + ) +``` + +6. **Generate Tokens and Prepare Response** +```python + # 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:** +```json +{ + "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** +```sql +id | username | email | first_name | last_name | is_active +---|----------------------|----------------------|------------|-----------|---------- +2 | sarah@company.com | sarah@company.com | Sarah | Smith | true +``` + +**Table: igny8_tenants (accounts)** +```sql +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** +```sql +id | name | slug | price | included_credits | max_sites +---|---------------|---------|----------|------------------|---------- +2 | Starter Plan | starter | 5000.00 | 5000 | 3 +``` + +**Table: igny8_subscriptions** +```sql +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** +```sql +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** +```sql +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** +```sql +-- EMPTY (No credits allocated yet, waiting for payment approval) +``` + +**Table: igny8_payments** +```sql +-- 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:** +```typescript +// File: frontend/src/components/billing/PendingPaymentBanner.tsx + +export const PendingPaymentBanner: React.FC = () => { + const { user } = useAuthStore(); + const [invoice, setInvoice] = useState(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 ( +
+
+
+

+ Payment Required +

+

+ Invoice #{invoice.id} - ${invoice.total} {invoice.currency} due by {formatDate(invoice.due_date)} +

+

+ Payment Method: {invoice.payment_method} +

+
+ +
+ + {showModal && ( + setShowModal(false)} + onSuccess={() => { + setShowModal(false); + // Refresh invoice status + fetchPendingInvoice(); + }} + /> + )} +
+ ); +}; +``` + +**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:** +```typescript +// File: frontend/src/components/billing/PaymentConfirmationModal.tsx + +export const PaymentConfirmationModal: React.FC = ({ invoice, onClose, onSuccess }) => { + const [reference, setReference] = useState(''); + const [notes, setNotes] = useState(''); + const [file, setFile] = useState(null); + const [uploading, setUploading] = useState(false); + + const handleFileChange = (e: React.ChangeEvent) => { + 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 ( +
+
+

Confirm Your Payment

+ +
+

Invoice: #{invoice.id}

+

Amount: ${invoice.total} {invoice.currency}

+

Payment Method: {invoice.payment_method}

+
+ +
{ e.preventDefault(); handleSubmit(); }}> +
+ + setReference(e.target.value)} + placeholder="e.g., JC-20241209-789456" + required + /> +
+ +
+ +