Files
igny8/IMPLEMENTATION-PLAN-SIGNUP-TO-PAYMENT-WORKFLOW.md
IGNY8 VPS (Salman) 9f85ce4f52 sadasd
2025-12-08 18:22:10 +00:00

127 KiB

IMPLEMENTATION PLAN: Clean Signup to Payment Workflow

Version: 2.0
Date: December 8, 2025
Status: 🔧 READY FOR IMPLEMENTATION


Table of Contents

  1. Executive Summary
  2. Deep Analysis Findings
  3. Field & Relationship Audit
  4. Simplified Model Architecture
  5. Complete Workflow Diagrams
  6. Implementation Phases
  7. Database Migrations
  8. Testing Plan

Executive Summary

Current State Analysis

Critical Issues Found:

  1. Duplicate Date Fields - Period dates stored in 2 places (Subscription + Invoice)
  2. Payment Method Chaos - Stored in 3 different models with no single source of truth
  3. Missing Relationships - Subscription has no plan field, breaking invoice creation
  4. Unused Payment References - No external_payment_id captured for manual payments
  5. Profile Fields Not Updated - Billing info never synced from account to invoices
  6. Country-Specific Logic Missing - Pakistan payment methods not filtered
  7. Site Industry Not Required - Can create sites without industry, then can't add sectors
  8. Broken Paid Signup Flow - Imports non-existent billing.Subscription
  9. Credits Not Single Source - Account.credits, Plan.included_credits not synchronized
  10. Invoice PDF Missing - No pdf_file field, no proper PDF generation/download
  11. No Billing Settings Section - Collect minimal data in signup, need account settings for full billing info

Complexity Issues:

  • Payment method filtering by region not properly implemented
  • Payment method config table exists but not used in signup flow
  • Manual payment instructions not shown to users
  • No clear workflow for payment confirmation after signup
  • Credits stored in multiple places instead of single source
  • Invoice PDF field missing, no download capability
  • Billing data collected but no account settings section for updates

Target Architecture

Simplified Models:

Account (1) ──────┐
  ├─ plan_id      │
  ├─ credits      │
  └─ status       │
                  │
Subscription (1)  │◄── ONE-TO-ONE relationship
  ├─ account_id   │
  ├─ plan_id      │◄── ADDED (currently missing)
  ├─ period_start │◄── SINGLE source of truth for dates
  ├─ period_end   │
  └─ status       │
                  │
Invoice (many)    │◄── Created from Subscription
  ├─ subscription_id
  ├─ total        │◄── No duplicate period dates
  └─ status       │
                  │
Payment (many)    │◄── Links to Invoice
  ├─ invoice_id   │
  ├─ payment_method
  ├─ external_payment_id  ◄── ADDED for manual payments
  └─ status       │

Simplified Payment Methods:

  • Global (2): Stripe, PayPal (available worldwide)
  • Regional (2):
    • Bank Transfer (UK, USA, Canada, Europe only)
    • Local Wallet (Pakistan only - JazzCash/Easypaisa)
  • Total: 4 payment methods maximum per region

Deep Analysis Findings

1. Date Field Redundancy

Problem: Period dates duplicated in multiple places

Current State:

# Subscription model (auth/models.py line 253-254)
current_period_start = models.DateTimeField()
current_period_end = models.DateTimeField()

# Invoice model (billing/models.py line 212-213)
billing_period_start = models.DateTimeField(null=True, blank=True)
billing_period_end = models.DateTimeField(null=True, blank=True)

Issues:

  • Same period stored in 2 places
  • Can become out of sync
  • Invoice fields are nullable but subscription fields are required
  • No automatic sync mechanism

Solution: Keep dates in Subscription only (single source of truth)
Remove billing_period_start/end from Invoice
Access dates via invoice.subscription.current_period_start

Migration Required:

# 1. Remove fields from Invoice model
migrations.RemoveField('invoice', 'billing_period_start')
migrations.RemoveField('invoice', 'billing_period_end')

# No data loss - dates preserved in Subscription

2. Payment Method Field Chaos

Problem: Payment method stored in 3 different models with no clear hierarchy

Current Locations:

# 1. Account (auth/models.py line 87-92)
payment_method = models.CharField(
    max_length=30,
    choices=PAYMENT_METHOD_CHOICES,
    default='stripe'
)

# 2. Subscription (auth/models.py line 240-244)
payment_method = models.CharField(
    max_length=30,
    choices=PAYMENT_METHOD_CHOICES,
    default='stripe'
)

# 3. Payment (billing/models.py line 309)
payment_method = models.CharField(
    max_length=50,
    choices=PAYMENT_METHOD_CHOICES,
    db_index=True
)

# 4. AccountPaymentMethod (billing/models.py line 476) ✓ CORRECT
type = models.CharField(
    max_length=50,
    choices=PAYMENT_METHOD_CHOICES
)

Which is Used?

  • Registration: Sets Account.payment_method and creates AccountPaymentMethod
  • Subscription creation: Copies from account or uses 'bank_transfer' default
  • Payment processing: Uses Payment.payment_method
  • Result: 3 fields that can be out of sync

Solution: Use AccountPaymentMethod as single source of truth
Deprecate Account.payment_method (make it read-only derived property)
Deprecate Subscription.payment_method (get from account's default method)
Keep Payment.payment_method for historical record

New Pattern:

# Account model
@property
def payment_method(self):
    """Get default payment method from AccountPaymentMethod"""
    default_method = self.accountpaymentmethod_set.filter(
        is_default=True, 
        is_enabled=True
    ).first()
    return default_method.type if default_method else 'stripe'

# Subscription model - remove field, use property
@property
def payment_method(self):
    return self.account.payment_method

3. Missing Subscription.plan Relationship

Problem: Subscription has no plan field, breaking invoice logic

Current State:

# Subscription model (auth/models.py line 218-260)
class Subscription(models.Model):
    account = models.OneToOneField('Account')
    # ❌ NO PLAN FIELD
    stripe_subscription_id = CharField
    payment_method = CharField
    status = CharField
    current_period_start = DateTimeField
    current_period_end = DateTimeField

Code Expects Plan:

# InvoiceService.create_subscription_invoice()
subscription.plan  # ❌ AttributeError: 'Subscription' object has no attribute 'plan'

Why It's Missing:

  • Plan stored in Account.plan only
  • Assumption: Get plan via subscription.account.plan
  • Problem: If account changes plan mid-subscription, historical invoices show wrong plan

Solution: Add Subscription.plan field
Set from Account.plan at subscription creation
Keep plan locked for subscription duration (historical accuracy)
New subscription created when plan changes

Migration:

migrations.AddField(
    model_name='subscription',
    name='plan',
    field=models.ForeignKey(
        'igny8_core_auth.Plan',
        on_delete=models.PROTECT,
        related_name='subscriptions',
        null=True  # Temporarily nullable
    ),
)

# Data migration: Copy from account
def copy_plan_from_account(apps, schema_editor):
    Subscription = apps.get_model('igny8_core_auth', 'Subscription')
    for sub in Subscription.objects.all():
        sub.plan = sub.account.plan
        sub.save()

# Make required
migrations.AlterField(
    model_name='subscription',
    name='plan',
    field=models.ForeignKey(
        'igny8_core_auth.Plan',
        on_delete=models.PROTECT,
        related_name='subscriptions'
    ),
)

4. Missing Payment References for Manual Payments

Problem: No way to track bank transfer references or local wallet transaction IDs

Current State:

# Payment model has these fields:
stripe_payment_intent_id = CharField  # ✓ For Stripe
stripe_charge_id = CharField          # ✓ For Stripe
paypal_order_id = CharField           # ✓ For PayPal
paypal_capture_id = CharField         # ✓ For PayPal

# For manual payments:
manual_reference = CharField(blank=True)  # ✓ EXISTS but not used
transaction_reference = CharField(blank=True)  # ⚠️ DUPLICATE field

Issues:

  • Two fields for same purpose (manual_reference and transaction_reference)
  • Neither field is populated during signup
  • No validation requiring reference for manual payments
  • Admin approval has no reference number to verify

Solution: Keep manual_reference only (remove transaction_reference)
Require manual_reference for bank_transfer and local_wallet payments
Show reference field in payment confirmation form
Display reference in admin approval interface

Validation:

class Payment(models.Model):
    def clean(self):
        # Require manual_reference for manual payment methods
        if self.payment_method in ['bank_transfer', 'local_wallet']:
            if not self.manual_reference:
                raise ValidationError({
                    'manual_reference': 'Reference number required for manual payments'
                })

5. Billing Profile Fields Not Updated

Problem: Account billing fields exist but never populated or synced

Current Account Fields:

# Account model (auth/models.py line 100-110)
billing_email = models.EmailField(blank=True, null=True)
billing_address_line1 = CharField(max_length=255, blank=True)
billing_address_line2 = CharField(max_length=255, blank=True)
billing_city = CharField(max_length=100, blank=True)
billing_state = CharField(max_length=100, blank=True)
billing_postal_code = CharField(max_length=20, blank=True)
billing_country = CharField(max_length=2, blank=True)
tax_id = CharField(max_length=100, blank=True)

When Are They Set? Not set during registration
Not set during payment
No form to update them
Invoice has duplicate billing_email field

Solution: Add billing form during paid plan signup (before payment)
Update account billing fields when user confirms payment
Snapshot billing info to Invoice.metadata at invoice creation
Remove duplicate Invoice.billing_email field

Signup Flow Update:

1. User selects paid plan
2. User fills registration form
3. User fills billing info form ← NEW STEP
4. User selects payment method
5. Payment instructions shown
6. Account created with billing info
7. Invoice created with billing snapshot

6. Payment Method Country Logic Missing

Problem: Pakistan-specific payment method not filtered by country

Current Setup:

# PaymentMethodConfig model exists (billing/models.py line 410)
class PaymentMethodConfig(models.Model):
    country_code = CharField  # e.g., 'PK'
    payment_method = CharField  # e.g., 'local_wallet'
    is_enabled = BooleanField
    instructions = TextField
    wallet_type = CharField  # e.g., 'JazzCash', 'Easypaisa'

Issues:

  • Config exists but NOT used in signup flow
  • Frontend shows all 4 payment methods to everyone
  • No country detection
  • No filtering logic

Solution: Detect user country from IP or billing_country
Query PaymentMethodConfig for available methods
Show only enabled methods for user's country
Create default configs for global methods

Default Configurations:

# Global methods (available everywhere)
PaymentMethodConfig.objects.create(
    country_code='*',  # Wildcard for all countries
    payment_method='stripe',
    is_enabled=True,
    display_name='Credit/Debit Card (Stripe)',
    sort_order=1
)

PaymentMethodConfig.objects.create(
    country_code='*',
    payment_method='paypal',
    is_enabled=True,
    display_name='PayPal',
    sort_order=2
)

PaymentMethodConfig.objects.create(
    country_code='*',
    payment_method='bank_transfer',
    is_enabled=True,
    display_name='Bank Transfer',
    instructions='Transfer to: Account 123456...',
    sort_order=3
)

# Pakistan-specific
PaymentMethodConfig.objects.create(
    country_code='PK',
    payment_method='local_wallet',
    is_enabled=True,
    display_name='JazzCash / Easypaisa',
    wallet_type='JazzCash',
    wallet_id='03001234567',
    instructions='Send payment to JazzCash: 03001234567',
    sort_order=4
)

API Endpoint:

# New endpoint: GET /v1/billing/payment-methods/?country=PK
def list_payment_methods(request):
    country = request.GET.get('country', '*')
    
    # Get country-specific + global methods
    methods = PaymentMethodConfig.objects.filter(
        Q(country_code=country) | Q(country_code='*'),
        is_enabled=True
    ).order_by('sort_order')
    
    return Response(PaymentMethodConfigSerializer(methods, many=True).data)

7. Site.industry Field Not Required

Problem: Industry is nullable but required for sector creation

Current State:

# Site model (auth/models.py line 280-286)
industry = models.ForeignKey(
    'Industry',
    on_delete=models.PROTECT,
    related_name='sites',
    null=True,  # ❌ NULLABLE
    blank=True,
    help_text="Industry this site belongs to"
)

Serializer:

# SiteSerializer (auth/serializers.py line 60-82)
class Meta:
    fields = ['industry', ...]
    # ❌ industry not in required_fields

Impact:

  • Sites created without industry
  • Sector creation fails: if self.industry_sector.industry != self.site.industry
  • If site.industry is None, comparison always fails
  • User can't add any sectors

Solution: Make Site.industry required (not nullable)
Update migration to set default industry for existing NULL sites
Update serializer to require industry
Update frontend to show industry selector in site creation form

Migration:

# 1. Set default industry for existing sites
def set_default_industry(apps, schema_editor):
    Site = apps.get_model('igny8_core_auth', 'Site')
    Industry = apps.get_model('igny8_core_auth', 'Industry')
    
    default_industry = Industry.objects.filter(
        slug='technology'
    ).first()
    
    if default_industry:
        Site.objects.filter(industry__isnull=True).update(
            industry=default_industry
        )

migrations.RunPython(set_default_industry)

# 2. Make field required
migrations.AlterField(
    model_name='site',
    name='industry',
    field=models.ForeignKey(
        'Industry',
        on_delete=models.PROTECT,
        related_name='sites'
        # Removed: null=True, blank=True
    ),
)

8. Broken Paid Signup Import

Problem: Imports non-existent billing.Subscription

Current Code:

# auth/serializers.py line 291
from igny8_core.business.billing.models import Subscription  # ❌ DOES NOT EXIST

Fix:

# Use existing auth.Subscription
from igny8_core.auth.models import Subscription  # ✓ EXISTS

Field & Relationship Audit

Complete Field Inventory

Subscription Model - BEFORE Cleanup

class Subscription(models.Model):
    # Relationships
    account = OneToOneField('Account')          # ✓ KEEP
    # ❌ MISSING: plan field
    
    # Payment tracking
    stripe_subscription_id = CharField          # ✓ KEEP
    payment_method = CharField                  # ❌ REMOVE (use AccountPaymentMethod)
    external_payment_id = CharField             # ✓ KEEP
    
    # Status & dates
    status = CharField                          # ✓ KEEP
    current_period_start = DateTimeField        # ✓ KEEP (source of truth)
    current_period_end = DateTimeField          # ✓ KEEP (source of truth)
    cancel_at_period_end = BooleanField         # ✓ KEEP
    
    # Audit
    created_at = DateTimeField                  # ✓ KEEP
    updated_at = DateTimeField                  # ✓ KEEP

Subscription Model - AFTER Cleanup

class Subscription(models.Model):
    # Relationships
    account = OneToOneField('Account')          # ✓ Tenant isolation
    plan = ForeignKey('Plan')                   # ✅ ADDED - historical plan tracking
    
    # Payment tracking
    stripe_subscription_id = CharField          # ✓ Stripe reference
    external_payment_id = CharField             # ✓ Manual payment reference
    
    # Status & dates (SINGLE SOURCE OF TRUTH)
    status = CharField                          # ✓ active/past_due/canceled
    current_period_start = DateTimeField        # ✓ Billing cycle start
    current_period_end = DateTimeField          # ✓ Billing cycle end
    cancel_at_period_end = BooleanField         # ✓ Cancellation flag
    
    # Audit
    created_at = DateTimeField
    updated_at = DateTimeField
    
    # Properties (derived, not stored)
    @property
    def payment_method(self):
        """Get from AccountPaymentMethod"""
        return self.account.payment_method

Changes Summary:

  • Added: plan field (FK to Plan)
  • Removed: payment_method field (use property instead)
  • Field count: 11 → 11 (same, but cleaner relationships)

Invoice Model - BEFORE Cleanup

class Invoice(AccountBaseModel):
    # Relationships
    account = ForeignKey('Account')             # ✓ KEEP (via AccountBaseModel)
    subscription = ForeignKey('Subscription')   # ✓ KEEP
    
    # Amounts
    subtotal = DecimalField                     # ✓ KEEP
    tax = DecimalField                          # ✓ KEEP
    total = DecimalField                        # ✓ KEEP
    currency = CharField                        # ✓ KEEP
    
    # Status & dates
    status = CharField                          # ✓ KEEP
    invoice_number = CharField                  # ✓ KEEP
    invoice_date = DateField                    # ✓ KEEP
    due_date = DateField                        # ✓ KEEP
    paid_at = DateTimeField                     # ✓ KEEP
    
    # DUPLICATE FIELDS
    billing_period_start = DateTimeField        # ❌ REMOVE (in Subscription)
    billing_period_end = DateTimeField          # ❌ REMOVE (in Subscription)
    billing_email = EmailField                  # ❌ REMOVE (use metadata snapshot)
    payment_method = CharField                  # ✓ KEEP (historical record)
    
    # Data
    line_items = JSONField                      # ✓ KEEP
    notes = TextField                           # ✓ KEEP
    metadata = JSONField                        # ✓ KEEP (billing snapshot goes here)
    
    # Stripe
    stripe_invoice_id = CharField               # ✓ KEEP
    
    # Audit
    created_at = DateTimeField                  # ✓ KEEP
    updated_at = DateTimeField                  # ✓ KEEP

Invoice Model - AFTER Cleanup

class Invoice(AccountBaseModel):
    # Relationships
    account = ForeignKey('Account')             # ✓ Tenant isolation
    subscription = ForeignKey('Subscription')   # ✓ Links to subscription
    
    # Invoice identity
    invoice_number = CharField(unique=True)     # ✓ INV-2025-001
    
    # Amounts
    subtotal = DecimalField                     # ✓ Pre-tax amount
    tax = DecimalField                          # ✓ Tax amount
    total = DecimalField                        # ✓ Total payable
    currency = CharField(default='USD')         # ✓ Currency code
    
    # Status & dates
    status = CharField                          # ✓ draft/pending/paid/void
    invoice_date = DateField                    # ✓ Issue date
    due_date = DateField                        # ✓ Payment deadline
    paid_at = DateTimeField(null=True)          # ✓ Payment timestamp
    
    # Historical tracking
    payment_method = CharField                  # ✓ Payment method used
    
    # Data
    line_items = JSONField(default=list)        # ✓ Invoice items
    notes = TextField(blank=True)               # ✓ Admin notes
    metadata = JSONField(default=dict)          # ✓ Billing snapshot
    
    # Stripe integration
    stripe_invoice_id = CharField               # ✓ Stripe reference
    
    # Audit
    created_at = DateTimeField
    updated_at = DateTimeField
    
    # Properties (access via relationship)
    @property
    def billing_period_start(self):
        """Get from subscription"""
        return self.subscription.current_period_start if self.subscription else None
    
    @property
    def billing_period_end(self):
        """Get from subscription"""
        return self.subscription.current_period_end if self.subscription else None
    
    @property
    def billing_email(self):
        """Get from metadata snapshot or account"""
        return self.metadata.get('billing_email') or self.account.billing_email

Changes Summary:

  • Removed: billing_period_start (get from subscription)
  • Removed: billing_period_end (get from subscription)
  • Removed: billing_email (use metadata snapshot)
  • Field count: 21 → 18 (3 fields removed, cleaner)

Simplified Model Architecture

Core Models Summary

Model Fields (Before) Fields (After) Changes
Account 26 fields 26 fields Convert payment_method to property
Subscription 11 fields 11 fields Add plan, Remove payment_method field
Invoice 21 fields 18 fields Remove 3 duplicate fields
Payment 26 fields 25 fields Remove transaction_reference
AccountPaymentMethod 11 fields 11 fields No changes (already correct)
PaymentMethodConfig 14 fields 14 fields No changes (needs implementation)

Total Field Reduction: 109 fields → 105 fields (4 fewer fields, cleaner relationships)


Relationship Map - BEFORE Cleanup

Account (1)
  ├─ plan_id ──────────────────► Plan
  ├─ payment_method (field)     ⚠️ Can be out of sync
  │
  ├─ Subscription (1:1)
  │    ├─ payment_method (field)  ⚠️ Duplicate #2
  │    ├─ period_start
  │    ├─ period_end
  │    └─ ❌ NO plan field
  │
  ├─ Invoice (1:many)
  │    ├─ subscription_id
  │    ├─ period_start           ⚠️ Duplicate dates
  │    ├─ period_end             ⚠️ Duplicate dates
  │    ├─ billing_email          ⚠️ Duplicate field
  │    └─ payment_method (field) ⚠️ Duplicate #3
  │
  ├─ Payment (1:many)
  │    ├─ invoice_id
  │    ├─ payment_method (field) ⚠️ Duplicate #4
  │    ├─ manual_reference
  │    └─ transaction_reference   ⚠️ Duplicate field
  │
  └─ AccountPaymentMethod (1:many)  ✓ Correct design
       ├─ type
       ├─ is_default
       └─ is_enabled

Problems:

  • 🔴 4 payment_method fields (which is source of truth?)
  • 🔴 Period dates duplicated (Subscription + Invoice)
  • 🔴 Billing email duplicated (Account + Invoice)
  • 🔴 Payment references duplicated (manual_reference + transaction_reference)
  • 🔴 No plan in Subscription (can't track plan changes)

Relationship Map - AFTER Cleanup

Account (1)
  ├─ plan_id ──────────────────► Plan
  │  @property payment_method → get from AccountPaymentMethod
  │
  ├─ Subscription (1:1)
  │    ├─ ✅ plan_id ──────────► Plan (added)
  │    ├─ period_start          ◄─── SINGLE SOURCE
  │    ├─ period_end            ◄─── SINGLE SOURCE
  │    └─ @property payment_method → get from Account
  │
  ├─ Invoice (1:many)
  │    ├─ subscription_id ─────► Subscription
  │    ├─ @property period_start  → via subscription
  │    ├─ @property period_end    → via subscription
  │    ├─ @property billing_email → via metadata
  │    └─ payment_method (field)  ✓ Historical record
  │
  ├─ Payment (1:many)
  │    ├─ invoice_id ──────────► Invoice
  │    ├─ payment_method (field)  ✓ Historical record
  │    └─ manual_reference        ✓ Single field
  │
  └─ AccountPaymentMethod (1:many) ◄─── PRIMARY SOURCE
       ├─ type                    ◄─── Source of truth
       ├─ is_default
       ├─ is_enabled
       └─ instructions

PaymentMethodConfig (global)
  ├─ country_code
  ├─ payment_method
  ├─ is_enabled
  └─ instructions

Solutions:

  • AccountPaymentMethod is single source of truth
  • Subscription has plan_id for historical tracking
  • Period dates only in Subscription (Invoice uses properties)
  • Billing email only in Account (Invoice snapshots to metadata)
  • Single payment reference field
  • Clear hierarchy: Config → AccountPaymentMethod → Historical records

Complete Workflow Diagrams

1. FREE TRIAL SIGNUP FLOW

┌────────────────────────────────────────────────────────────────┐
│ USER: Clicks "Start Free Trial" on homepage                   │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│ FRONTEND: /signup (no plan parameter)                         │
│                                                                 │
│  Form Fields:                                                   │
│  ├─ First Name*                                                │
│  ├─ Last Name*                                                 │
│  ├─ Email*                                                     │
│  ├─ Password*                                                  │
│  ├─ Account Name (optional)                                   │
│  └─ ☑ Agree to Terms                                          │
│                                                                 │
│  [Create Account] button                                       │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│ API: POST /v1/auth/register/                                   │
│                                                                 │
│  Request Body:                                                  │
│  {                                                              │
│    "email": "user@example.com",                                │
│    "password": "SecurePass123!",                               │
│    "first_name": "John",                                       │
│    "last_name": "Doe",                                         │
│    "account_name": "John's Business"                           │
│    // plan_slug not provided = defaults to 'free'             │
│  }                                                              │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│ BACKEND: RegisterSerializer.create()                           │
│                                                                 │
│  1. Resolve Plan                                               │
│     plan = Plan.objects.get(slug='free')                      │
│     status = 'trial'                                           │
│     credits = 1000 (plan.included_credits)                    │
│                                                                 │
│  2. Generate Username                                          │
│     username = email.split('@')[0]                            │
│     (ensure unique with counter if needed)                     │
│                                                                 │
│  3. Create User (without account first)                        │
│     user = User.objects.create_user(                          │
│       username=username,                                       │
│       email=email,                                             │
│       password=hashed_password,                                │
│       role='owner',                                            │
│       account=None  ← Will be set later                       │
│     )                                                           │
│                                                                 │
│  4. Generate Account Slug                                      │
│     slug = slugify(account_name)                              │
│     (ensure unique with counter)                               │
│                                                                 │
│  5. Create Account                                             │
│     account = Account.objects.create(                         │
│       name=account_name,                                       │
│       slug=slug,                                               │
│       owner=user,                                              │
│       plan=plan,                                               │
│       credits=1000,                                            │
│       status='trial'                                           │
│     )                                                           │
│                                                                 │
│  6. Link User to Account                                       │
│     user.account = account                                     │
│     user.save()                                                │
│                                                                 │
│  7. Log Credit Transaction                                     │
│     CreditTransaction.objects.create(                         │
│       account=account,                                         │
│       transaction_type='subscription',                        │
│       amount=1000,                                             │
│       balance_after=1000,                                      │
│       description='Free plan credits'                         │
│     )                                                           │
│                                                                 │
│  8. Return User Object                                         │
│     return user                                                │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│ FRONTEND: Check account status                                 │
│                                                                 │
│  if (user.account.status === 'trial') {                        │
│    navigate('/sites')  ← Go to dashboard                      │
│  }                                                              │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│ USER: Lands on /sites (Sites page)                            │
│                                                                 │
│  - Can create 1 site (plan.max_sites = 1)                     │
│  - Has 1000 credits to use                                     │
│  - Status: Free Trial                                          │
└────────────────────────────────────────────────────────────────┘

Database State After Free Trial Signup:

-- igny8_tenants (Accounts)
INSERT INTO igny8_tenants VALUES (
  1, 'Johns Business', 'johns-business', 1, 1000, 'trial', NULL
);
-- id, name, slug, plan_id, credits, status, owner_id

-- igny8_users
INSERT INTO igny8_users VALUES (
  1, 'john@example.com', 'john-doe', 'owner', 1
);
-- id, email, username, role, tenant_id

-- igny8_credit_transactions
INSERT INTO igny8_credit_transactions VALUES (
  1, 1, 'subscription', 1000, 1000, 'Free plan credits'
);
-- id, tenant_id, type, amount, balance_after, description

-- igny8_subscriptions: NO RECORD (free trial has no subscription)
-- igny8_invoices: NO RECORD
-- igny8_payments: NO RECORD

2. PAID PLAN SIGNUP FLOW (With Manual Payment)

┌────────────────────────────────────────────────────────────────┐
│ USER: Clicks "Get Started" on Starter plan pricing card       │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│ FRONTEND: /signup?plan=starter                                 │
│                                                                 │
│  Page loads with plan details shown:                           │
│  ┌──────────────────────────────────────┐                     │
│  │ 📋 Starter Plan                       │                     │
│  │ $29/month                             │                     │
│  │ ✓ 5,000 credits                      │                     │
│  │ ✓ 3 sites                             │                     │
│  │ ✓ 3 users                             │                     │
│  └──────────────────────────────────────┘                     │
│                                                                 │
│  Form Fields:                                                   │
│  ├─ First Name*                                                │
│  ├─ Last Name*                                                 │
│  ├─ Email*                                                     │
│  ├─ Password*                                                  │
│  ├─ Account Name*                                              │
│  └─ ☑ Agree to Terms                                          │
│                                                                 │
│  [Continue to Billing] button                                  │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│ FRONTEND: /signup/billing (New Step)                          │
│                                                                 │
│  Billing Information Form:                                      │
│  ├─ Billing Email*                                             │
│  ├─ Country* (dropdown with flag icons)                       │
│  ├─ Address Line 1                                             │
│  ├─ Address Line 2                                             │
│  ├─ City                                                       │
│  ├─ State/Province                                             │
│  ├─ Postal Code                                                │
│  └─ Tax ID (optional)                                          │
│                                                                 │
│  [Continue to Payment] button                                  │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│ FRONTEND: Fetch Available Payment Methods                     │
│                                                                 │
│  API: GET /v1/billing/payment-methods/?country=PK             │
│                                                                 │
│  Response:                                                      │
│  [                                                              │
│    {                                                            │
│      "payment_method": "stripe",                               │
│      "display_name": "Credit/Debit Card",                     │
│      "sort_order": 1                                           │
│    },                                                           │
│    {                                                            │
│      "payment_method": "paypal",                               │
│      "display_name": "PayPal",                                 │
│      "sort_order": 2                                           │
│    },                                                           │
│    {                                                            │
│      "payment_method": "bank_transfer",                        │
│      "display_name": "Bank Transfer",                          │
│      "instructions": "Transfer to: Account 123456789...",     │
│      "sort_order": 3                                           │
│    },                                                           │
│    {                                                            │
│      "payment_method": "local_wallet",                         │
│      "display_name": "JazzCash / Easypaisa",                  │
│      "wallet_type": "JazzCash",                                │
│      "wallet_id": "03001234567",                               │
│      "instructions": "Send to JazzCash: 03001234567",         │
│      "sort_order": 4                                           │
│    }                                                            │
│  ]                                                              │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│ FRONTEND: /signup/payment                                      │
│                                                                 │
│  Payment Method Selection:                                      │
│  ○ Credit/Debit Card (Stripe)                                 │
│  ○ PayPal                                                      │
│  ● Bank Transfer ← User selects                               │
│  ○ JazzCash / Easypaisa                                        │
│                                                                 │
│  [Conditional: If Bank Transfer selected]                      │
│  ┌──────────────────────────────────────────────────────────┐ │
│  │ 📋 Bank Transfer Instructions                             │ │
│  │                                                            │ │
│  │ Account Name: IGNY8 Inc                                   │ │
│  │ Account Number: 123456789                                 │ │
│  │ Bank Name: ABC Bank                                       │ │
│  │ SWIFT: ABCPKKA                                            │ │
│  │                                                            │ │
│  │ Amount: $29.00 USD                                        │ │
│  │                                                            │ │
│  │ ⚠️ Important:                                             │ │
│  │ - Make payment to above account                           │ │
│  │ - Keep transaction reference                              │ │
│  │ - You'll confirm payment in next step                     │ │
│  └──────────────────────────────────────────────────────────┘ │
│                                                                 │
│  [Create Account & Continue] button                            │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│ API: POST /v1/auth/register/                                   │
│                                                                 │
│  Request Body:                                                  │
│  {                                                              │
│    "email": "user@example.com",                                │
│    "password": "SecurePass123!",                               │
│    "first_name": "Ahmad",                                      │
│    "last_name": "Khan",                                        │
│    "account_name": "Ahmad Tech",                               │
│    "plan_slug": "starter",                                     │
│    "payment_method": "bank_transfer",                          │
│    "billing_email": "billing@ahmad.com",                       │
│    "billing_country": "PK",                                    │
│    "billing_address_line1": "123 Main St",                     │
│    "billing_city": "Karachi"                                   │
│  }                                                              │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│ BACKEND: RegisterSerializer.create() - PAID PLAN PATH         │
│                                                                 │
│  1. Resolve Plan                                               │
│     plan = Plan.objects.get(slug='starter')                   │
│     status = 'pending_payment'                                 │
│     credits = 0  ← No credits until payment confirmed         │
│                                                                 │
│  2. Calculate Period Dates                                     │
│     period_start = timezone.now()                              │
│     period_end = period_start + 30 days                        │
│                                                                 │
│  3. Create User                                                │
│     user = User.objects.create_user(...)                      │
│                                                                 │
│  4. Create Account WITH Billing Info                           │
│     account = Account.objects.create(                         │
│       name="Ahmad Tech",                                       │
│       slug="ahmad-tech",                                       │
│       owner=user,                                              │
│       plan=plan,                                               │
│       credits=0,                                               │
│       status='pending_payment',                                │
│       billing_email="billing@ahmad.com",                       │
│       billing_country="PK",                                    │
│       billing_address_line1="123 Main St",                     │
│       billing_city="Karachi"                                   │
│     )                                                           │
│                                                                 │
│  5. Link User to Account                                       │
│     user.account = account                                     │
│     user.save()                                                │
│                                                                 │
│  6. Create Subscription ✅ WITH PLAN                           │
│     subscription = Subscription.objects.create(                │
│       account=account,                                         │
│       plan=plan,  ← ADDED FIELD                               │
│       status='pending_payment',                                │
│       current_period_start=period_start,                       │
│       current_period_end=period_end,                           │
│       cancel_at_period_end=False                               │
│     )                                                           │
│                                                                 │
│  7. Create Invoice                                             │
│     invoice = InvoiceService.create_subscription_invoice(      │
│       subscription=subscription,                               │
│       billing_period_start=period_start,                       │
│       billing_period_end=period_end                            │
│     )                                                           │
│     # Invoice fields:                                          │
│     # - invoice_number: "INV-2025-001"                         │
│     # - total: 29.00                                           │
│     # - status: "pending"                                      │
│     # - metadata: { billing snapshot }                         │
│                                                                 │
│  8. Create AccountPaymentMethod                                │
│     AccountPaymentMethod.objects.create(                      │
│       account=account,                                         │
│       type='bank_transfer',                                    │
│       display_name='Bank Transfer (Manual)',                   │
│       is_default=True,                                         │
│       is_enabled=True,                                         │
│       instructions='See invoice for details'                   │
│     )                                                           │
│                                                                 │
│  9. NO CREDIT TRANSACTION (not yet paid)                       │
│                                                                 │
│  10. Return User Object                                        │
│      return user                                               │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│ FRONTEND: Check account status                                 │
│                                                                 │
│  if (user.account.status === 'pending_payment') {              │
│    navigate('/account/plans')  ← Go to payment confirmation   │
│  }                                                              │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│ USER: Lands on /account/plans (Account & Plans Page)          │
│                                                                 │
│  Account Status Bar:                                            │
│  ┌──────────────────────────────────────────────────────────┐ │
│  │ ⚠️  Payment Pending                                       │ │
│  │ Your account is pending payment confirmation              │ │
│  │ [Confirm Payment] button                                  │ │
│  └──────────────────────────────────────────────────────────┘ │
│                                                                 │
│  Current Plan: Starter Plan ($29/month)                        │
│  Status: Pending Payment                                       │
│  Credits: 0 / 5,000                                            │
│                                                                 │
│  Invoices Tab:                                                  │
│  ┌──────────────────────────────────────────────────────────┐ │
│  │ Invoice #INV-2025-001                                     │ │
│  │ Date: Dec 8, 2025                                         │ │
│  │ Amount: $29.00                                            │ │
│  │ Status: Pending                                           │ │
│  │ Payment Method: Bank Transfer                             │ │
│  │ [Confirm Payment] button                                  │ │
│  └──────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘

Database State After Paid Signup (Before Payment Confirmation):

-- igny8_tenants
INSERT INTO igny8_tenants VALUES (
  2, 'Ahmad Tech', 'ahmad-tech', 2, 0, 'pending_payment', 
  'billing@ahmad.com', 'PK', '123 Main St', NULL, 'Karachi', NULL, NULL, NULL
);
-- id, name, slug, plan_id, credits, status, billing_email, billing_country, address...

-- igny8_users
INSERT INTO igny8_users VALUES (
  2, 'user@example.com', 'ahmad-khan', 'owner', 2
);

-- igny8_subscriptions ✅ WITH PLAN NOW
INSERT INTO igny8_subscriptions VALUES (
  2, 2, 2, NULL, 'pending_payment', '2025-12-08', '2026-01-08', FALSE
);
-- id, tenant_id, plan_id, stripe_id, status, period_start, period_end, cancel_at_end

-- igny8_invoices
INSERT INTO igny8_invoices VALUES (
  2, 2, 2, 'INV-2025-002', 29.00, 0, 29.00, 'USD', 'pending', '2025-12-08', '2025-12-15'
);
-- id, tenant_id, subscription_id, invoice_number, subtotal, tax, total, currency, status, invoice_date, due_date

-- igny8_account_payment_methods
INSERT INTO igny8_account_payment_methods VALUES (
  2, 2, 'bank_transfer', 'Bank Transfer (Manual)', TRUE, TRUE, FALSE
);
-- id, tenant_id, type, display_name, is_default, is_enabled, is_verified

-- igny8_payments: NO RECORD YET
-- igny8_credit_transactions: NO RECORD YET

3. PAYMENT CONFIRMATION FLOW (Manual Payment)

┌────────────────────────────────────────────────────────────────┐
│ USER: Clicks "Confirm Payment" button                          │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│ FRONTEND: Payment Confirmation Modal/Page                      │
│                                                                 │
│  ┌──────────────────────────────────────────────────────────┐ │
│  │ 💳 Confirm Payment                                        │ │
│  │                                                            │ │
│  │ Invoice: #INV-2025-002                                    │ │
│  │ Amount: $29.00 USD                                        │ │
│  │ Payment Method: Bank Transfer                             │ │
│  │                                                            │ │
│  │ Payment Reference Number:*                                │ │
│  │ ┌──────────────────────────────────────────────────────┐ │ │
│  │ │ [Enter transaction reference]                         │ │ │
│  │ └──────────────────────────────────────────────────────┘ │ │
│  │                                                            │ │
│  │ Payment Date:*                                            │ │
│  │ ┌──────────────────────────────────────────────────────┐ │ │
│  │ │ [Select date]                                         │ │ │
│  │ └──────────────────────────────────────────────────────┘ │ │
│  │                                                            │ │
│  │ Additional Notes:                                         │ │
│  │ ┌──────────────────────────────────────────────────────┐ │ │
│  │ │ [Optional notes about payment]                        │ │ │
│  │ └──────────────────────────────────────────────────────┘ │ │
│  │                                                            │ │
│  │ Upload Proof (Optional):                                  │ │
│  │ ┌──────────────────────────────────────────────────────┐ │ │
│  │ │ [Upload receipt/screenshot]                           │ │ │
│  │ └──────────────────────────────────────────────────────┘ │ │
│  │                                                            │ │
│  │ ⚠️ Your payment will be reviewed by our team within 24hrs│ │
│  │                                                            │ │
│  │ [Cancel]  [Submit for Approval]                          │ │
│  └──────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│ API: POST /v1/billing/payments/confirm/                        │
│                                                                 │
│  Request Body:                                                  │
│  {                                                              │
│    "invoice_id": 2,                                            │
│    "payment_method": "bank_transfer",                          │
│    "manual_reference": "BT-20251208-12345",                    │
│    "manual_notes": "Paid via ABC Bank on Dec 8",              │
│    "amount": "29.00",                                          │
│    "proof_url": "https://s3.../receipt.jpg"                    │
│  }                                                              │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│ BACKEND: Create Payment Record - Pending Approval              │
│                                                                 │
│  Payment.objects.create(                                        │
│    account=account,                                            │
│    invoice=invoice,                                            │
│    amount=29.00,                                               │
│    currency='USD',                                             │
│    status='pending_approval',  ← Awaiting admin approval      │
│    payment_method='bank_transfer',                             │
│    manual_reference='BT-20251208-12345',  ← User provided     │
│    manual_notes='Paid via ABC Bank on Dec 8',                 │
│    metadata={                                                  │
│      'proof_url': 'https://s3.../receipt.jpg'                 │
│    }                                                            │
│  )                                                              │
│                                                                 │
│  # Invoice remains "pending" until payment approved            │
│  # Account remains "pending_payment" until payment approved    │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│ FRONTEND: Success Message                                      │
│                                                                 │
│  ┌──────────────────────────────────────────────────────────┐ │
│  │ ✅ Payment Submitted                                      │ │
│  │                                                            │ │
│  │ Your payment confirmation has been submitted for review.  │ │
│  │ We'll verify your payment and activate your account       │ │
│  │ within 24 hours.                                          │ │
│  │                                                            │ │
│  │ Reference: BT-20251208-12345                              │ │
│  │ Status: Pending Review                                    │ │
│  │                                                            │ │
│  │ You'll receive an email once approved.                    │ │
│  │                                                            │ │
│  │ [Go to Dashboard]                                         │ │
│  └──────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│ USER: Dashboard shows "Payment under review" status            │
│                                                                 │
│  - Can browse platform but can't create sites                 │
│  - Credits still 0 until approved                              │
│  - Status shows "Pending Approval"                             │
└────────────────────────────────────────────────────────────────┘

Database State After Payment Submission:

-- igny8_payments (NEW RECORD)
INSERT INTO igny8_payments VALUES (
  1, 2, 2, 29.00, 'USD', 'pending_approval', 'bank_transfer',
  NULL, NULL, NULL, NULL,
  'BT-20251208-12345', 'Paid via ABC Bank on Dec 8',
  NULL, NULL, NULL, NULL, NULL, NULL,
  '{"proof_url": "https://s3.../receipt.jpg"}'
);
-- id, tenant_id, invoice_id, amount, currency, status, payment_method,
-- stripe fields, paypal fields,
-- manual_reference, manual_notes,
-- admin_notes, approved_by, approved_at, processed_at, failed_at, refunded_at,
-- metadata

4. ADMIN APPROVAL FLOW

┌────────────────────────────────────────────────────────────────┐
│ ADMIN: Receives notification of pending payment                │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│ ADMIN PANEL: /admin/billing/payment/                           │
│                                                                 │
│  Pending Payments List:                                         │
│  ┌──────────────────────────────────────────────────────────┐ │
│  │ Payment #1                                                │ │
│  │ Account: Ahmad Tech                                       │ │
│  │ Invoice: INV-2025-002                                     │ │
│  │ Amount: $29.00                                            │ │
│  │ Method: Bank Transfer                                     │ │
│  │ Reference: BT-20251208-12345                              │ │
│  │ Notes: Paid via ABC Bank on Dec 8                        │ │
│  │ Proof: [View Receipt]                                     │ │
│  │                                                            │ │
│  │ [Approve] [Reject]                                        │ │
│  └──────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│ ADMIN: Clicks "Approve" button                                 │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│ API: POST /v1/billing/payments/1/approve/                      │
│                                                                 │
│  Request Body:                                                  │
│  {                                                              │
│    "admin_notes": "Verified payment in bank statement"         │
│  }                                                              │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│ BACKEND: Approve Payment & Activate Account                    │
│                                                                 │
│  with transaction.atomic():                                     │
│    # 1. Update Payment                                         │
│    payment.status = 'succeeded'                                │
│    payment.approved_by = admin_user                            │
│    payment.approved_at = timezone.now()                        │
│    payment.processed_at = timezone.now()                       │
│    payment.admin_notes = 'Verified payment...'                 │
│    payment.save()                                              │
│                                                                 │
│    # 2. Update Invoice                                         │
│    invoice.status = 'paid'                                     │
│    invoice.paid_at = timezone.now()                            │
│    invoice.save()                                              │
│                                                                 │
│    # 3. Update Subscription                                    │
│    subscription.status = 'active'                              │
│    subscription.external_payment_id = payment.manual_reference │
│    subscription.save()                                         │
│                                                                 │
│    # 4. Update Account                                         │
│    account.status = 'active'                                   │
│    account.save()                                              │
│                                                                 │
│    # 5. Add Credits                                            │
│    CreditService.add_credits(                                  │
│      account=account,                                          │
│      amount=subscription.plan.included_credits,  # 5000        │
│      description=f'Starter plan credits - {invoice.invoice_number}',│
│      metadata={                                                │
│        'subscription_id': subscription.id,                     │
│        'invoice_id': invoice.id,                               │
│        'payment_id': payment.id                                │
│      }                                                          │
│    )                                                            │
│    # This creates:                                             │
│    # - Updates account.credits = 5000                          │
│    # - Creates CreditTransaction record                        │
│                                                                 │
│    # 6. Send Activation Email                                  │
│    send_account_activated_email(account, subscription)        │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│ USER: Receives email "Account Activated"                       │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│ USER: Logs in and sees activated account                       │
│                                                                 │
│  Account Status: ✅ Active                                     │
│  Plan: Starter Plan                                            │
│  Credits: 5,000 / 5,000                                        │
│  Sites: 0 / 3                                                  │
│                                                                 │
│  [Create Your First Site] button                               │
└────────────────────────────────────────────────────────────────┘

Database State After Admin Approval:

-- igny8_tenants (UPDATED)
UPDATE igny8_tenants SET status = 'active', credits = 5000 WHERE id = 2;

-- igny8_subscriptions (UPDATED)
UPDATE igny8_subscriptions SET status = 'active', external_payment_id = 'BT-20251208-12345' WHERE id = 2;

-- igny8_invoices (UPDATED)
UPDATE igny8_invoices SET status = 'paid', paid_at = '2025-12-08 15:30:00' WHERE id = 2;

-- igny8_payments (UPDATED)
UPDATE igny8_payments SET 
  status = 'succeeded', 
  approved_by = 1, 
  approved_at = '2025-12-08 15:30:00',
  processed_at = '2025-12-08 15:30:00',
  admin_notes = 'Verified payment in bank statement'
WHERE id = 1;

-- igny8_credit_transactions (NEW RECORD)
INSERT INTO igny8_credit_transactions VALUES (
  2, 2, 'subscription', 5000, 5000, 'Starter plan credits - INV-2025-002',
  '{"subscription_id": 2, "invoice_id": 2, "payment_id": 1}'
);

5. SITE CREATION FLOW (After Account Activated)

┌────────────────────────────────────────────────────────────────┐
│ USER: Clicks "Create Your First Site" button                   │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│ FRONTEND: /sites/create                                        │
│                                                                 │
│  Site Creation Form:                                            │
│  ├─ Site Name*                                                 │
│  │  [Enter site name]                                          │
│  │                                                             │
│  ├─ Domain/URL                                                 │
│  │  [https://example.com]                                      │
│  │                                                             │
│  ├─ Industry* ✅ REQUIRED FIELD                                │
│  │  [Select industry ▼]                                        │
│  │   - Technology                                              │
│  │   - Healthcare                                              │
│  │   - Finance                                                 │
│  │   - E-commerce                                              │
│  │   ...                                                       │
│  │                                                             │
│  ├─ Description                                                │
│  │  [Describe your site...]                                    │
│  │                                                             │
│  └─ Site Type                                                  │
│     ○ Marketing Site                                           │
│     ○ Blog                                                     │
│     ○ E-commerce                                               │
│     ○ Corporate                                                │
│                                                                 │
│  [Cancel] [Create Site]                                        │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│ API: POST /v1/auth/sites/                                      │
│                                                                 │
│  Request Body:                                                  │
│  {                                                              │
│    "name": "Tech News Hub",                                    │
│    "domain": "https://technewshub.com",                        │
│    "industry": 2,  ← REQUIRED (Technology industry ID)        │
│    "description": "Latest technology news and tutorials",      │
│    "site_type": "blog"                                         │
│  }                                                              │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│ BACKEND: SiteViewSet.create()                                  │
│                                                                 │
│  1. Validate Account Plan Limits                               │
│     max_sites = account.plan.max_sites  # 3 for Starter       │
│     current_sites = account.sites.filter(is_active=True).count()│
│     if current_sites >= max_sites:                             │
│       raise ValidationError("Site limit reached")              │
│                                                                 │
│  2. Validate Industry Required ✅                              │
│     if not validated_data.get('industry'):                     │
│       raise ValidationError("Industry is required")            │
│                                                                 │
│  3. Generate Unique Slug                                       │
│     slug = slugify("Tech News Hub")  # "tech-news-hub"        │
│     # Ensure unique per account                                │
│                                                                 │
│  4. Normalize Domain                                           │
│     domain = "https://technewshub.com"                         │
│     # Ensure https:// prefix                                   │
│                                                                 │
│  5. Create Site                                                │
│     site = Site.objects.create(                               │
│       account=request.account,  # Auto from middleware        │
│       name="Tech News Hub",                                    │
│       slug="tech-news-hub",                                    │
│       domain="https://technewshub.com",                        │
│       industry_id=2,  # Technology                            │
│       description="Latest technology news...",                 │
│       site_type="blog",                                        │
│       hosting_type="igny8_sites",                              │
│       status="active",                                         │
│       is_active=True                                           │
│     )                                                           │
│                                                                 │
│  6. Create SiteUserAccess ✅ NEW STEP                          │
│     SiteUserAccess.objects.create(                            │
│       user=request.user,                                       │
│       site=site,                                               │
│       granted_by=request.user                                  │
│     )                                                           │
│                                                                 │
│  7. Return Site Object                                         │
│     return site                                                │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│ FRONTEND: Redirect to /sites/{site_id}/sectors                │
│                                                                 │
│  Success message: "Site created! Now add sectors to organize   │
│                    your content."                              │
└────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌────────────────────────────────────────────────────────────────┐
│ USER: Add Sectors to Site                                      │
│                                                                 │
│  Available Sectors for Technology Industry:                     │
│  □ Web Development                                             │
│  □ AI & Machine Learning                                       │
│  □ Cybersecurity                                               │
│  □ Cloud Computing                                             │
│  □ Mobile Development                                          │
│                                                                 │
│  Select up to 5 sectors (Starter plan limit)                   │
│  [Add Selected Sectors]                                        │
└────────────────────────────────────────────────────────────────┘

Database State After Site Creation:

-- igny8_sites (NEW RECORD)
INSERT INTO igny8_sites VALUES (
  1, 2, 'Tech News Hub', 'tech-news-hub', 'https://technewshub.com',
  'Latest technology news and tutorials', 2, TRUE, 'active',
  'blog', 'igny8_sites'
);
-- id, tenant_id, name, slug, domain, description, industry_id, is_active, status, site_type, hosting_type

-- igny8_site_user_access (NEW RECORD) ✅
INSERT INTO igny8_site_user_access VALUES (
  1, 2, 1, 2, '2025-12-08 16:00:00'
);
-- id, user_id, site_id, granted_by_id, granted_at

Implementation Phases

PHASE 1: Critical Fixes (URGENT - 1 Day)

Priority: 🔴 Blocking Production

1.1 Fix Subscription Import

# File: backend/igny8_core/auth/serializers.py
# Line 291

# BEFORE:
from igny8_core.business.billing.models import Subscription  # ❌

# AFTER:
from igny8_core.auth.models import Subscription  # ✅

1.2 Add Subscription.plan Field

cd backend
python manage.py makemigrations --name add_subscription_plan_field

# Migration file created: 0XXX_add_subscription_plan_field.py
# Migration content:
from django.db import migrations, models

def copy_plan_from_account(apps, schema_editor):
    """Copy plan from account to subscription"""
    Subscription = apps.get_model('igny8_core_auth', 'Subscription')
    for sub in Subscription.objects.select_related('account__plan'):
        sub.plan = sub.account.plan
        sub.save(update_fields=['plan'])

class Migration(migrations.Migration):
    dependencies = [
        ('igny8_core_auth', '0XXX_previous_migration'),
    ]
    
    operations = [
        # 1. Add field as nullable
        migrations.AddField(
            model_name='subscription',
            name='plan',
            field=models.ForeignKey(
                'igny8_core_auth.Plan',
                on_delete=models.PROTECT,
                related_name='subscriptions',
                null=True
            ),
        ),
        # 2. Copy data
        migrations.RunPython(
            copy_plan_from_account,
            reverse_code=migrations.RunPython.noop
        ),
        # 3. Make non-nullable
        migrations.AlterField(
            model_name='subscription',
            name='plan',
            field=models.ForeignKey(
                'igny8_core_auth.Plan',
                on_delete=models.PROTECT,
                related_name='subscriptions'
            ),
        ),
    ]
# Apply migration
python manage.py migrate

1.3 Make Site.industry Required

python manage.py makemigrations --name make_site_industry_required
def set_default_industry(apps, schema_editor):
    """Set default industry for sites without one"""
    Site = apps.get_model('igny8_core_auth', 'Site')
    Industry = apps.get_model('igny8_core_auth', 'Industry')
    
    default = Industry.objects.filter(slug='technology').first()
    if default:
        Site.objects.filter(industry__isnull=True).update(industry=default)

class Migration(migrations.Migration):
    operations = [
        migrations.RunPython(set_default_industry),
        migrations.AlterField(
            model_name='site',
            name='industry',
            field=models.ForeignKey(
                'igny8_core_auth.Industry',
                on_delete=models.PROTECT,
                related_name='sites'
            ),
        ),
    ]

1.4 Update Free Plan Credits

docker exec igny8_backend python manage.py shell
from igny8_core.auth.models import Plan
plan = Plan.objects.get(slug='free')
plan.included_credits = 1000
plan.save()
print(f"Updated free plan credits: {plan.included_credits}")

Testing Phase 1:

# Test 1: Free trial signup
curl -X POST http://localhost:8000/api/v1/auth/register/ \
  -H "Content-Type: application/json" \
  -d '{
    "email": "test@example.com",
    "password": "Test123!",
    "password_confirm": "Test123!",
    "first_name": "Test",
    "last_name": "User"
  }'
# Expected: ✅ Success, 1000 credits

# Test 2: Paid signup (should not crash)
curl -X POST http://localhost:8000/api/v1/auth/register/ \
  -H "Content-Type: application/json" \
  -d '{
    "email": "paid@example.com",
    "password": "Test123!",
    "password_confirm": "Test123!",
    "first_name": "Paid",
    "last_name": "User",
    "plan_slug": "starter"
  }'
# Expected: ✅ Success, subscription created with plan_id

PHASE 2: Model Cleanup (2-3 Days)

Priority: 🟡 Important

2.1 Remove Duplicate Fields from Invoice

python manage.py makemigrations --name remove_duplicate_invoice_fields
class Migration(migrations.Migration):
    operations = [
        migrations.RemoveField('invoice', 'billing_period_start'),
        migrations.RemoveField('invoice', 'billing_period_end'),
        migrations.RemoveField('invoice', 'billing_email'),
    ]

2.2 Add Properties to Invoice Model

# File: backend/igny8_core/business/billing/models.py

class Invoice(AccountBaseModel):
    # ... existing fields ...
    
    @property
    def billing_period_start(self):
        """Get from subscription"""
        return self.subscription.current_period_start if self.subscription else None
    
    @property
    def billing_period_end(self):
        """Get from subscription"""
        return self.subscription.current_period_end if self.subscription else None
    
    @property
    def billing_email(self):
        """Get from metadata or account"""
        snapshot = self.metadata.get('billing_snapshot', {})
        return snapshot.get('email') or self.account.billing_email

2.3 Remove Duplicate payment_method from Subscription

python manage.py makemigrations --name remove_subscription_payment_method
class Migration(migrations.Migration):
    operations = [
        migrations.RemoveField('subscription', 'payment_method'),
    ]

2.4 Add payment_method Property to Subscription

# File: backend/igny8_core/auth/models.py

class Subscription(models.Model):
    # ... existing fields ...
    
    @property
    def payment_method(self):
        """Get from account's default payment method"""
        return self.account.payment_method

2.5 Convert Account.payment_method to Property

# File: backend/igny8_core/auth/models.py

class Account(SoftDeletableModel):
    # Keep field for backward compatibility but make it read-only in forms
    # Eventually remove via migration
    
    @property
    def default_payment_method(self):
        """Get default payment method from AccountPaymentMethod"""
        method = self.accountpaymentmethod_set.filter(
            is_default=True,
            is_enabled=True
        ).first()
        return method.type if method else 'stripe'

2.6 Remove transaction_reference from Payment

python manage.py makemigrations --name remove_duplicate_payment_reference
class Migration(migrations.Migration):
    operations = [
        migrations.RemoveField('payment', 'transaction_reference'),
    ]

PHASE 3: New Features (3-5 Days)

Priority: 🟢 Enhancement

3.1 Create PaymentMethodConfig Default Data

docker exec igny8_backend python manage.py shell
from igny8_core.business.billing.models import PaymentMethodConfig

# Global methods (available worldwide)
PaymentMethodConfig.objects.get_or_create(
    country_code='*',
    payment_method='stripe',
    defaults={
        'is_enabled': True,
        'display_name': 'Credit/Debit Card (Stripe)',
        'sort_order': 1
    }
)

PaymentMethodConfig.objects.get_or_create(
    country_code='*',
    payment_method='paypal',
    defaults={
        'is_enabled': True,
        'display_name': 'PayPal',
        'sort_order': 2
    }
)

# UK, USA, Canada, Europe - Bank Transfer
for country in ['GB', 'US', 'CA', 'DE', 'FR', 'IT', 'ES', 'NL', 'BE', 'AT', 'IE', 'SE', 'NO', 'DK', 'FI']:
    PaymentMethodConfig.objects.get_or_create(
        country_code=country,
        payment_method='bank_transfer',
        defaults={
            'is_enabled': True,
            'display_name': 'Bank Transfer',
            'instructions': '''
                Bank Name: ABC Bank
                Account Name: IGNY8 Inc
                Account Number: 123456789
                SWIFT/BIC: ABCPKKA
                IBAN: GB00ABCD12345678901234
                
                Please transfer the exact invoice amount and include your invoice number as reference.
            ''',
            'sort_order': 3
        }
    )

# Pakistan - Local Wallet (JazzCash/Easypaisa)
PaymentMethodConfig.objects.get_or_create(
    country_code='PK',
    payment_method='local_wallet',
    defaults={
        'is_enabled': True,
        'display_name': 'JazzCash / Easypaisa',
        'wallet_type': 'JazzCash',
        'wallet_id': '03001234567',
        'instructions': '''
            Send payment to:
            JazzCash: 03001234567
            Account Title: IGNY8
            
            After payment:
            1. Note the Transaction ID
            2. Submit confirmation below with Transaction ID
            3. Upload screenshot (optional)
        ''',
        'sort_order': 4
    }
)

print("Payment method configurations created!")
print("- Global: Stripe, PayPal")
print("- UK/USA/Canada/Europe: + Bank Transfer")
print("- Pakistan: + JazzCash/Easypaisa")

3.2 Create Payment Methods API Endpoint

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

@action(detail=False, methods=['get'], url_path='payment-methods')
def list_payment_methods(self, request):
    """Get available payment methods for user's country"""
    country = request.GET.get('country', '*')
    
    # Get country-specific + global methods
    from django.db.models import Q
    methods = PaymentMethodConfig.objects.filter(
        Q(country_code=country) | Q(country_code='*'),
        is_enabled=True
    ).order_by('sort_order')
    
    from .serializers import PaymentMethodConfigSerializer
    return Response(PaymentMethodConfigSerializer(methods, many=True).data)

3.3 Create Payment Confirmation Endpoint

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

@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')
    
    if not invoice_id or not manual_reference:
        return error_response(
            error='invoice_id and manual_reference required',
            status_code=400,
            request=request
        )
    
    try:
        invoice = Invoice.objects.get(
            id=invoice_id,
            account=request.account
        )
        
        payment = Payment.objects.create(
            account=request.account,
            invoice=invoice,
            amount=invoice.total,
            currency=invoice.currency,
            status='pending_approval',
            payment_method=invoice.payment_method or 'bank_transfer',
            manual_reference=manual_reference,
            manual_notes=manual_notes,
            metadata={'proof_url': proof_url} if proof_url else {}
        )
        
        # Send notification to admin
        send_payment_confirmation_notification(payment)
        
        return success_response(
            data={'payment_id': payment.id, 'status': 'pending_approval'},
            message='Payment confirmation submitted for review',
            request=request
        )
    except Invoice.DoesNotExist:
        return error_response(
            error='Invoice not found',
            status_code=404,
            request=request
        )

NOTE: RegisterSerializer already handles signup correctly with existing billing fields in Account model. No changes needed.


3.4 Admin Payment Approval Interface

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

from django.contrib import admin
from .models import Payment

@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"""
        count = 0
        for payment in queryset.filter(status='pending_approval'):
            try:
                # Call existing approval endpoint
                from .views import BillingViewSet
                viewset = BillingViewSet()
                viewset.request = request
                viewset.approve_payment_internal(payment)
                count += 1
            except Exception as e:
                self.message_user(
                    request,
                    f"Error approving payment {payment.id}: {str(e)}",
                    level='ERROR'
                )
        
        self.message_user(
            request,
            f"Successfully approved {count} payment(s)"
        )
    
    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})
        
        self.message_user(request, f"Rejected {count} payment(s)")
    
    reject_payments.short_description = "Reject selected payments"

PHASE 4: Testing & Validation (1-2 Days)

Priority: 🔵 Quality Assurance

4.1 Test: Free Trial Signup End-to-End

# 1. Frontend: Go to /signup
# 2. Fill form: email, password, name
# 3. Submit
# 4. Verify: Account created with 1000 credits
# 5. Verify: Subscription.plan_id = 1 (Free plan)
# 6. Verify: Can create 1 site
# 7. Verify: Site requires industry selection
# 8. Verify: Can access site dashboard

Database Verification:

SELECT 
  a.id, a.email, a.status, a.credits,
  s.plan_id, s.status, s.current_period_start,
  p.slug, p.included_credits
FROM igny8_tenants a
JOIN igny8_subscriptions s ON s.tenant_id = a.id
JOIN igny8_plans p ON s.plan_id = p.id
WHERE a.email = 'test@example.com';
-- Expected: credits=1000, plan_id=1, plan.slug='free'

4.2 Test: Paid Signup with Bank Transfer

# 1. Frontend: /signup?plan=starter
# 2. Fill signup form
# 3. Step 2: Fill billing form
#    - billing_email: billing@company.com
#    - billing_country: PK
#    - payment_method: bank_transfer
# 4. Submit
# 5. Verify: Account created with status='pending_payment'
# 6. Verify: Invoice created with total=5000
# 7. Verify: Payment method instructions shown
# 8. User submits payment with reference: BT-12345
# 9. Verify: Payment status='pending_approval'
# 10. Admin approves payment
# 11. Verify: Account status='active', credits=5000
# 12. User can now create 3 sites

API Test:

# Step 1: Register with paid plan
curl -X POST http://localhost:8000/api/v1/auth/register/ \
  -H "Content-Type: application/json" \
  -d '{
    "email": "paid@test.com",
    "password": "Test123!",
    "password_confirm": "Test123!",
    "first_name": "Paid",
    "last_name": "User",
    "plan_slug": "starter",
    "billing_email": "billing@test.com",
    "billing_country": "PK",
    "payment_method": "bank_transfer"
  }'

# Expected response:
# {
#   "success": true,
#   "message": "Account created. Please complete payment.",
#   "data": {
#     "account_id": 2,
#     "invoice_id": 2,
#     "amount": 5000,
#     "payment_method": "bank_transfer",
#     "instructions": "Bank transfer details..."
#   }
# }

# Step 2: Confirm payment
curl -X POST http://localhost:8000/api/v1/billing/payments/confirm/ \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "invoice_id": 2,
    "manual_reference": "BT-20251208-12345",
    "manual_notes": "Transferred via ABC Bank on Dec 8"
  }'

# Expected:
# {
#   "success": true,
#   "message": "Payment confirmation submitted for review",
#   "data": {"payment_id": 1, "status": "pending_approval"}
# }

4.3 Test: Site Creation with Industry

# After account activated, create site
curl -X POST http://localhost:8000/api/v1/auth/sites/ \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My Tech Blog",
    "domain": "https://mytechblog.com",
    "industry": 2,
    "site_type": "blog"
  }'

# Expected: ✅ Site created

# Test without industry (should fail)
curl -X POST http://localhost:8000/api/v1/auth/sites/ \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My Blog",
    "domain": "https://myblog.com"
  }'

# Expected: ❌ 400 Bad Request, "industry is required"

4.4 Test: Payment Method Filtering by Country

# Get payment methods for Pakistan
curl http://localhost:8000/api/v1/billing/payment-methods/?country=PK

# Expected:
# [
#   {"type": "stripe", "display_name": "Credit/Debit Card (Stripe)"},
#   {"type": "paypal", "display_name": "PayPal"},
#   {"type": "bank_transfer", "display_name": "Bank Transfer", "instructions": "..."},
#   {"type": "local_wallet", "display_name": "JazzCash / Easypaisa", "instructions": "..."}
# ]

# Get payment methods for USA
curl http://localhost:8000/api/v1/billing/payment-methods/?country=US

# Expected (only global methods):
# [
#   {"type": "stripe", "display_name": "Credit/Debit Card (Stripe)"},
#   {"type": "paypal", "display_name": "PayPal"},
#   {"type": "bank_transfer", "display_name": "Bank Transfer"}
# ]

Summary: Complete Workflow After Implementation

FREE TRIAL PATH:
User → /signup → Fill form → Submit → Account created (1000 credits) → 
Create site (industry required) → Add sectors → Start using

PAID PLAN PATH:
User → /signup?plan=starter → Fill signup form → Fill billing form → 
Submit → Account created (pending_payment) → Invoice generated → 
User sees payment instructions → Transfer payment → Submit confirmation → 
Admin receives notification → Admin approves → Account activated (5000 credits) → 
Create sites (up to 3) → Add sectors → Start using

Key Improvements: Subscription.plan field tracks historical plan No duplicate date fields (Invoice uses Subscription dates) Single source of truth for payment methods (AccountPaymentMethod) Billing fields captured and saved during signup Country-specific payment methods shown Manual payment confirmation workflow Admin approval triggers atomic updates across all tables Site.industry is required SiteUserAccess created automatically Free plan has correct 1000 credits

Database Reduction:

  • Before: 109 fields across models
  • After: 105 fields (-4 duplicate fields)
  • Relationships cleaner: payment_method in 1 place instead of 4

Testing Coverage:

  • Free trial signup (frontend → API → database)
  • Paid plan signup with billing form
  • Payment confirmation flow
  • Admin approval workflow
  • Site creation with industry requirement
  • Payment method filtering by country
  • Credit allocation and transaction logging

Implementation Timeline

Phase Duration Tasks
Phase 1 1 day Fix import, add Subscription.plan, make Site.industry required, update free plan credits
Phase 2 2-3 days Remove duplicate fields, add properties, consolidate payment methods
Phase 3 3-5 days Create payment methods API, payment confirmation, billing form, admin interface
Phase 4 1-2 days End-to-end testing, validation, bug fixes
Total 7-11 days Complete implementation with testing

Critical Path:

  1. Day 1: Phase 1 (fixes blocking production)
  2. Day 2-4: Phase 2 (cleanup duplicate fields)
  3. Day 5-9: Phase 3 (new features)
  4. Day 10-11: Phase 4 (testing)

Appendix: Quick Reference

Model Changes Summary

# Subscription
+ plan = FK(Plan)  # NEW - for historical tracking
- payment_method  # REMOVE (use property from AccountPaymentMethod)

# Invoice
+ pdf_file = FileField  # NEW - generated PDF storage
- billing_period_start  # REMOVE (use property from subscription)
- billing_period_end  # REMOVE (use property from subscription)
- billing_email  # REMOVE (use property from metadata/account)

# Payment
+ is_current_period = BooleanField  # NEW - flag for current billing period
- transaction_reference  # REMOVE (duplicate of manual_reference)

# Account
+ billing_company, tax_id, billing_phone, billing_state, billing_postal_code  # NEW optional fields
! credits - SINGLE SOURCE, populated from plan.included_credits

# Site
! industry = FK(Industry, null=False)  # MAKE REQUIRED

# Plan
! included_credits - SOURCE OF TRUTH for all credit amounts

API Endpoints - New & Updated

New Endpoints:

GET  /v1/billing/payment-methods/?country={code}    # Filter methods by region
POST /v1/billing/payments/confirm/                  # Manual payment confirmation  
PATCH /v1/auth/account/billing                      # Update full billing details
GET  /v1/billing/payment-history                    # Complete payment history

Existing Endpoints (Already Implemented):

POST /v1/auth/register/                             # ✅ EXISTS - need to enhance with minimal billing
GET  /v1/billing/invoices/{id}/download_pdf/        # ✅ EXISTS - need to serve from pdf_file field
POST /v1/billing/confirm-bank-transfer              # ✅ EXISTS - admin manual payment approval
POST /v1/billing/payments/manual                    # ✅ EXISTS - user submits manual payment

Payment Method Regional Configuration:

Global (all countries):     Stripe, PayPal
UK/USA/Canada/Europe:       + Bank Transfer
Pakistan:                   + JazzCash/Easypaisa (local_wallet)

Database Queries for Verification

-- Verify subscription has plan
SELECT s.id, s.tenant_id, s.plan_id, p.slug
FROM igny8_subscriptions s
JOIN igny8_plans p ON s.plan_id = p.id;

-- Verify no duplicate payment methods
SELECT 
  a.id AS account_id,
  apm.type AS default_method,
  apm.is_default
FROM igny8_tenants a
LEFT JOIN igny8_account_payment_methods apm 
  ON apm.tenant_id = a.id AND apm.is_default = TRUE;

-- Verify all sites have industry
SELECT id, name, industry_id 
FROM igny8_sites 
WHERE industry_id IS NULL;
-- Expected: 0 rows

-- Verify credit transactions match account credits
SELECT 
  a.id,
  a.credits AS account_total,
  COALESCE(SUM(ct.amount), 0) AS transaction_total
FROM igny8_tenants a
LEFT JOIN igny8_credit_transactions ct ON ct.tenant_id = a.id
GROUP BY a.id, a.credits
HAVING a.credits != COALESCE(SUM(ct.amount), 0);
-- Expected: 0 rows (all match)

NOTE: Account model already has all billing fields (billing_email, billing_address_line1, billing_address_line2, billing_city, billing_state, billing_postal_code, billing_country). No new fields needed.


NOTE: Invoices already provide complete payment history. The InvoiceViewSet.list() endpoint returns all invoices with payment status. No separate payment history needed.


3.5 Invoice PDF Generation & Download

# File: backend/igny8_core/business/billing/models.py

class Invoice(AccountBaseModel):
    # ... existing fields ...
    
    # ADD NEW FIELD
    pdf_file = models.FileField(
        upload_to='invoices/pdf/%Y/%m/',
        null=True,
        blank=True,
        help_text='Generated PDF invoice file'
    )
    
    def generate_pdf(self):
        """Generate PDF and save to file field"""
        from .services.invoice_service import InvoiceService
        
        pdf_bytes = InvoiceService.generate_pdf(self)
        
        # Save to file field
        from django.core.files.base import ContentFile
        filename = f'invoice-{self.invoice_number}.pdf'
        self.pdf_file.save(filename, ContentFile(pdf_bytes), save=True)
        
        return self.pdf_file.url
    
    def save(self, *args, **kwargs):
        """Auto-generate PDF after invoice is saved"""
        super().save(*args, **kwargs)
        
        # Generate PDF for paid/draft invoices
        if self.status in ['paid', 'draft', 'sent'] and not self.pdf_file:
            self.generate_pdf()

Enhanced PDF Service:

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

from reportlab.lib.pagesizes import letter, A4
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image
from reportlab.lib.units import inch
from io import BytesIO
from django.conf import settings
import os

class InvoiceService:
    @staticmethod
    def generate_pdf(invoice):
        """Generate professional PDF invoice"""
        buffer = BytesIO()
        doc = SimpleDocTemplate(buffer, pagesize=A4, 
                                leftMargin=0.75*inch, rightMargin=0.75*inch,
                                topMargin=0.75*inch, bottomMargin=0.75*inch)
        
        elements = []
        styles = getSampleStyleSheet()
        
        # Company Header
        if os.path.exists(os.path.join(settings.STATIC_ROOT, 'logo.png')):
            logo = Image(os.path.join(settings.STATIC_ROOT, 'logo.png'), width=2*inch, height=0.5*inch)
            elements.append(logo)
        
        elements.append(Spacer(1, 0.3*inch))
        
        # Invoice Title
        title_style = ParagraphStyle(
            'CustomTitle',
            parent=styles['Heading1'],
            fontSize=24,
            textColor=colors.HexColor('#1a202c'),
            spaceAfter=30,
        )
        elements.append(Paragraph('INVOICE', title_style))
        
        # Invoice Info Table
        info_data = [
            ['Invoice Number:', invoice.invoice_number],
            ['Invoice Date:', invoice.invoice_date.strftime('%B %d, %Y')],
            ['Due Date:', invoice.due_date.strftime('%B %d, %Y')],
            ['Status:', invoice.status.upper()],
        ]
        
        if invoice.paid_at:
            info_data.append(['Paid On:', invoice.paid_at.strftime('%B %d, %Y')])
        
        info_table = Table(info_data, colWidths=[2*inch, 3*inch])
        info_table.setStyle(TableStyle([
            ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
            ('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
            ('FONTSIZE', (0, 0), (-1, -1), 10),
            ('BOTTOMPADDING', (0, 0), (-1, -1), 8),
        ]))
        elements.append(info_table)
        elements.append(Spacer(1, 0.4*inch))
        
        # Billing Info
        billing_style = ParagraphStyle(
            'BillingStyle',
            parent=styles['Normal'],
            fontSize=10,
            leading=14,
        )
        
        bill_to = f"<b>Bill To:</b><br/>"
        if invoice.account.billing_company:
            bill_to += f"{invoice.account.billing_company}<br/>"
        bill_to += f"{invoice.account.billing_email}<br/>"
        if invoice.account.billing_address:
            bill_to += f"{invoice.account.billing_address}<br/>"
            if invoice.account.billing_city:
                bill_to += f"{invoice.account.billing_city}, "
            if invoice.account.billing_state:
                bill_to += f"{invoice.account.billing_state} "
            if invoice.account.billing_postal_code:
                bill_to += f"{invoice.account.billing_postal_code}<br/>"
        if invoice.account.billing_country:
            bill_to += f"{invoice.account.billing_country}<br/>"
        if invoice.account.tax_id:
            bill_to += f"Tax ID: {invoice.account.tax_id}"
        
        elements.append(Paragraph(bill_to, billing_style))
        elements.append(Spacer(1, 0.4*inch))
        
        # Line Items Table
        line_items_data = [['Description', 'Quantity', 'Unit Price', 'Amount']]
        
        for item in invoice.line_items:
            line_items_data.append([
                item.get('description', ''),
                str(item.get('quantity', 1)),
                f"${item.get('unit_price', 0):.2f}",
                f"${item.get('amount', 0):.2f}"
            ])
        
        # Totals
        line_items_data.append(['', '', 'Subtotal:', f"${invoice.subtotal:.2f}"])
        if invoice.tax > 0:
            line_items_data.append(['', '', f'Tax ({invoice.tax_rate}%):', f"${invoice.tax:.2f}"])
        if invoice.discount > 0:
            line_items_data.append(['', '', 'Discount:', f"-${invoice.discount:.2f}"])
        line_items_data.append(['', '', 'Total:', f"${invoice.total:.2f}"])
        
        line_table = Table(line_items_data, colWidths=[3.5*inch, 1*inch, 1.5*inch, 1.5*inch])
        line_table.setStyle(TableStyle([
            ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#f7fafc')),
            ('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor('#2d3748')),
            ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
            ('ALIGN', (1, 1), (-1, -1), 'RIGHT'),
            ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
            ('FONTSIZE', (0, 0), (-1, 0), 11),
            ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
            ('BACKGROUND', (0, 1), (-1, -1), colors.white),
            ('GRID', (0, 0), (-1, -4), 0.5, colors.grey),
            ('LINEABOVE', (2, -3), (-1, -3), 1, colors.black),
            ('LINEABOVE', (2, -1), (-1, -1), 2, colors.black),
            ('FONTNAME', (2, -1), (-1, -1), 'Helvetica-Bold'),
            ('FONTSIZE', (2, -1), (-1, -1), 12),
        ]))
        elements.append(line_table)
        elements.append(Spacer(1, 0.5*inch))
        
        # Payment Info
        if invoice.notes:
            notes_style = ParagraphStyle(
                'NotesStyle',
                parent=styles['Normal'],
                fontSize=9,
                textColor=colors.HexColor('#4a5568'),
            )
            elements.append(Paragraph(f"<b>Notes:</b> {invoice.notes}", notes_style))
        
        # Footer
        elements.append(Spacer(1, 0.5*inch))
        footer_style = ParagraphStyle(
            'FooterStyle',
            parent=styles['Normal'],
            fontSize=8,
            textColor=colors.grey,
            alignment=1,  # Center
        )
        elements.append(Paragraph('Thank you for your business!', footer_style))
        elements.append(Paragraph('IGNY8 Inc. | support@igny8.com', footer_style))
        
        # Build PDF
        doc.build(elements)
        buffer.seek(0)
        return buffer.getvalue()

Updated Download Endpoint (already exists):

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

@action(detail=True, methods=['get'])
def download_pdf(self, request, pk=None):
    """Download invoice PDF - serves from file field if exists"""
    try:
        invoice = self.get_queryset().get(pk=pk)
        
        # Return existing PDF file if available
        if invoice.pdf_file:
            response = HttpResponse(invoice.pdf_file.read(), content_type='application/pdf')
            response['Content-Disposition'] = f'attachment; filename="invoice-{invoice.invoice_number}.pdf"'
            return response
        
        # Generate on-the-fly if not exists
        pdf_bytes = InvoiceService.generate_pdf(invoice)
        response = HttpResponse(pdf_bytes, content_type='application/pdf')
        response['Content-Disposition'] = f'attachment; filename="invoice-{invoice.invoice_number}.pdf"'
        return response
    except Invoice.DoesNotExist:
        return error_response(error='Invoice not found', status_code=404, request=request)

Migration for pdf_file field:

python manage.py makemigrations --name add_invoice_pdf_field
# Migration file
class Migration(migrations.Migration):
    operations = [
        migrations.AddField(
            model_name='invoice',
            name='pdf_file',
            field=models.FileField(
                upload_to='invoices/pdf/%Y/%m/',
                null=True,
                blank=True,
                help_text='Generated PDF invoice file'
            ),
        ),
    ]

3.9 Credits Single Source of Truth

# File: backend/igny8_core/auth/models.py

class Plan(models.Model):
    """
    Plan model - SOURCE OF TRUTH for credit amounts
    """
    included_credits = models.IntegerField(
        default=0,
        help_text='Monthly credits included in plan (source of truth)'
    )
    # ... other fields ...

class Account(SoftDeletableModel):
    """
    Account model - STORES CURRENT BALANCE only
    Credits populated from Plan.included_credits
    """
    plan = models.ForeignKey(Plan, on_delete=models.PROTECT)
    
    credits = models.IntegerField(
        default=0,
        help_text='Current credit balance (populated from plan.included_credits)'
    )
    
    @property
    def monthly_allowance(self):
        """Monthly allowance always from plan"""
        return self.plan.included_credits
    
    @property
    def total_credits_this_period(self):
        """Total credits for current billing period"""
        return self.plan.included_credits
    
    def reset_monthly_credits(self):
        """Reset credits at billing cycle - ALWAYS from plan"""
        self.credits = self.plan.included_credits
        self.save(update_fields=['credits'])
        
        # Log the reset
        CreditTransaction.objects.create(
            account=self,
            transaction_type='monthly_reset',
            amount=self.plan.included_credits,
            balance_after=self.credits,
            description=f'Monthly credit reset - {self.plan.name} plan',
            metadata={'plan_id': self.plan.id, 'included_credits': self.plan.included_credits}
        )

class Subscription(models.Model):
    """
    Subscription - NO credit fields, uses account.credits
    """
    account = models.OneToOneField(Account, on_delete=models.CASCADE)
    plan = models.ForeignKey(Plan, on_delete=models.PROTECT)  # For historical tracking
    
    # NO credits field here
    
    @property
    def remaining_credits(self):
        """Remaining credits from account"""
        return self.account.credits
    
    @property
    def monthly_allowance(self):
        """Monthly allowance from plan"""
        return self.plan.included_credits

class Invoice(AccountBaseModel):
    """
    Invoice - NO credit fields, reference from subscription.plan
    """
    subscription = models.ForeignKey(Subscription, on_delete=models.PROTECT, null=True)
    
    # NO included_credits field
    
    @property
    def plan_credits(self):
        """Credits from subscription's plan (historical snapshot)"""
        return self.subscription.plan.included_credits if self.subscription else 0

Credit Flow Documentation:

SINGLE SOURCE OF TRUTH: Plan.included_credits

Plan.included_credits (1000)
         │
         ├─→ Account.credits (populated on signup/renewal)
         │   └─→ Updated by CreditService.add_credits() / deduct_credits()
         │
         ├─→ Account.monthly_allowance (property, reads from plan)
         │
         └─→ Subscription.plan.included_credits (historical record)
             └─→ Invoice.plan_credits (property, from subscription.plan)

RULES:
1. Plan.included_credits = NEVER changes unless admin updates plan
2. Account.credits = CURRENT balance (can be spent/added to)
3. Account.monthly_allowance = ALWAYS reads from plan (property)
4. Monthly reset: Account.credits = Plan.included_credits
5. Invoice references: Use subscription.plan.included_credits (snapshot)

CreditService enforcement:

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

class CreditService:
    @staticmethod
    def add_credits(account, amount, description, transaction_type='manual'):
        """Add credits - updates ONLY account.credits"""
        with transaction.atomic():
            account.credits += amount
            account.save(update_fields=['credits'])
            
            CreditTransaction.objects.create(
                account=account,
                transaction_type=transaction_type,
                amount=amount,
                balance_after=account.credits,
                description=description
            )
    
    @staticmethod
    def reset_monthly_credits(account):
        """Reset to plan amount - SINGLE SOURCE"""
        new_balance = account.plan.included_credits
        
        with transaction.atomic():
            account.credits = new_balance
            account.save(update_fields=['credits'])
            
            CreditTransaction.objects.create(
                account=account,
                transaction_type='monthly_reset',
                amount=new_balance,
                balance_after=new_balance,
                description=f'Monthly reset - {account.plan.name} plan',
                metadata={
                    'plan_id': account.plan.id,
                    'plan_credits': account.plan.included_credits
                }
            )
# File: backend/igny8_core/business/billing/models.py

class Payment(AccountBaseModel):
    """
    Payment model - handles both one-time and recurring payments
    
    STRATEGY:
    - external_payment_id: Current active payment reference (Stripe sub ID, transaction ID)
    - manual_reference: User-provided reference for manual payments
    - invoice: Links payment to specific billing period
    - Multiple payments can exist for same subscription (historical)
    """
    
    invoice = models.ForeignKey(
        Invoice,
        on_delete=models.PROTECT,
        related_name='payments',
        help_text='Invoice this payment is for (links to billing period)'
    )
    
    external_payment_id = models.CharField(
        max_length=255,
        blank=True,
        help_text='External payment reference (Stripe charge ID, PayPal txn ID, etc.)'
    )
    
    manual_reference = models.CharField(
        max_length=255,
        blank=True,
        help_text='User-provided reference for manual payments (bank transfer ID, etc.)'
    )
    
    is_current_period = models.BooleanField(
        default=False,
        help_text='Is this payment for the current active billing period?'
    )
    
    @classmethod
    def get_current_period_payment(cls, account):
        """Get active payment for current billing period"""
        return cls.objects.filter(
            account=account,
            is_current_period=True,
            status='succeeded'
        ).first()
    
    @classmethod
    def get_payment_history(cls, account):
        """Get all historical payments"""
        return cls.objects.filter(
            account=account
        ).select_related('invoice').order_by('-created_at')

Recurring Payment Workflow:

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

class SubscriptionService:
    @staticmethod
    def renew_subscription(subscription):
        """
        Renew subscription - creates NEW invoice and payment
        Previous payments remain in history
        """
        with transaction.atomic():
            # Mark old payment as historical
            Payment.objects.filter(
                account=subscription.account,
                is_current_period=True
            ).update(is_current_period=False)
            
            # Create new invoice for new period
            new_invoice = Invoice.objects.create(
                account=subscription.account,
                subscription=subscription,
                invoice_number=f'INV-{timezone.now().strftime("%Y%m%d")}-{subscription.account.id}',
                total=subscription.plan.price,
                subtotal=subscription.plan.price,
                status='pending'
            )
            
            # Update subscription period
            subscription.current_period_start = timezone.now()
            subscription.current_period_end = timezone.now() + timedelta(days=30)
            subscription.save()
            
            # If auto-pay enabled, create payment
            if subscription.account.payment_method in ['stripe', 'paypal']:
                payment = Payment.objects.create(
                    account=subscription.account,
                    invoice=new_invoice,
                    amount=new_invoice.total,
                    payment_method=subscription.account.payment_method,
                    status='processing',
                    is_current_period=True
                )
                
                # Process payment via gateway
                # ... payment processing logic ...
                
            return new_invoice

Payment History View:

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

@action(detail=False, methods=['get'], url_path='payment-history')
def payment_history(self, request):
    """Get complete payment history with invoice details"""
    payments = Payment.objects.filter(
        account=request.account
    ).select_related('invoice', 'invoice__subscription').order_by('-created_at')
    
    results = []
    for payment in payments:
        results.append({
            'id': payment.id,
            'invoice_number': payment.invoice.invoice_number,
            'amount': str(payment.amount),
            'status': payment.status,
            'payment_method': payment.payment_method,
            'reference': payment.external_payment_id or payment.manual_reference,
            'is_current': payment.is_current_period,
            'billing_period': {
                'start': payment.invoice.subscription.current_period_start.isoformat(),
                'end': payment.invoice.subscription.current_period_end.isoformat()
            } if payment.invoice.subscription else None,
            'paid_at': payment.processed_at.isoformat() if payment.processed_at else None,
        })
    
    return success_response(data={'payments': results}, request=request)

Frontend Payment History:

// File: frontend/src/pages/BillingHistory.tsx

export const PaymentHistory = () => {
  const [payments, setPayments] = useState([]);
  
  return (
    <div>
      <h2>Payment History</h2>
      <table>
        <thead>
          <tr>
            <th>Date</th>
            <th>Invoice</th>
            <th>Amount</th>
            <th>Method</th>
            <th>Reference</th>
            <th>Status</th>
            <th>Period</th>
          </tr>
        </thead>
        <tbody>
          {payments.map(payment => (
            <tr key={payment.id} className={payment.is_current ? 'current-period' : ''}>
              <td>{formatDate(payment.paid_at)}</td>
              <td>{payment.invoice_number}</td>
              <td>${payment.amount}</td>
              <td>{payment.payment_method}</td>
              <td>{payment.reference}</td>
              <td>
                <Badge color={payment.status === 'succeeded' ? 'green' : 'yellow'}>
                  {payment.status}
                </Badge>
                {payment.is_current && <Badge color="blue">Current</Badge>}
              </td>
              <td>
                {payment.billing_period ? 
                  `${formatDate(payment.billing_period.start)} - ${formatDate(payment.billing_period.end)}` 
                  : 'N/A'}
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
};