From 33ad6768ec6f9e9ad2cec088fcea02a7ea5b50ae Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Mon, 8 Dec 2025 16:47:27 +0000 Subject: [PATCH] fina use and signup process --- CRITICAL-GAPS-SIGNUP-TO-SITE-WORKFLOW.md | 676 +++++ ...NTATION-PLAN-SIGNUP-TO-PAYMENT-WORKFLOW.md | 2488 +++++++++++++++++ 2 files changed, 3164 insertions(+) create mode 100644 CRITICAL-GAPS-SIGNUP-TO-SITE-WORKFLOW.md create mode 100644 IMPLEMENTATION-PLAN-SIGNUP-TO-PAYMENT-WORKFLOW.md diff --git a/CRITICAL-GAPS-SIGNUP-TO-SITE-WORKFLOW.md b/CRITICAL-GAPS-SIGNUP-TO-SITE-WORKFLOW.md new file mode 100644 index 00000000..b11bd209 --- /dev/null +++ b/CRITICAL-GAPS-SIGNUP-TO-SITE-WORKFLOW.md @@ -0,0 +1,676 @@ +# CRITICAL GAPS: Signup to Site Creation Workflow +**Analysis Date:** December 8, 2025 +**Status:** ๐Ÿ”ด BLOCKING ISSUES FOUND + +--- + +## Executive Summary + +**CRITICAL FINDING:** The registration flow for paid plans is **COMPLETELY BROKEN** due to missing model definition and multiple architectural inconsistencies. Free trial signups work but have significant gaps. + +**Impact:** +- โŒ Paid plan signups (starter/growth/scale) **FAIL** on registration +- โš ๏ธ Free trial signups work but create incomplete data structures +- โš ๏ธ Site creation has validation issues and missing relationships +- โš ๏ธ Duplicate fields cause data inconsistency risks + +--- + +## ๐Ÿ”ด CRITICAL ISSUES (Must Fix Immediately) + +### 1. MISSING MODEL: `billing.Subscription` Does Not Exist + +**Problem:** +`RegisterSerializer.create()` imports and tries to use `billing.Subscription` which **DOES NOT EXIST**: + +```python +# In auth/serializers.py line 291 +from igny8_core.business.billing.models import Subscription # โŒ IMPORT FAILS +``` + +**Evidence:** +```python +# Python shell test: +>>> from igny8_core.business.billing.models import Subscription +ImportError: cannot import name 'Subscription' from 'igny8_core.business.billing.models' +``` + +**What Actually Exists:** +- `auth.Subscription` model at `igny8_core/auth/models.py` line 218 +- Database table: `igny8_subscriptions` (created by `auth.Subscription`) + +**Impact:** +- Registration with paid plans (`starter`, `growth`, `scale`) **FAILS IMMEDIATELY** +- Line 403-412 in `RegisterSerializer.create()` crashes on paid signups: + ```python + subscription = Subscription.objects.create(...) # โŒ CRASHES + ``` + +**Root Cause:** +Documentation and code assume `billing.Subscription` was created but it was never implemented. + +**Fix Required:** +1. **Option A (Recommended):** Use existing `auth.Subscription` + ```python + # Change line 291 in auth/serializers.py + from igny8_core.auth.models import Subscription + ``` + +2. **Option B:** Create `billing.Subscription` and migrate + - Create model in `billing/models.py` + - Create migration to point Invoice FK to new model + - Data migration to copy existing records + - Update all imports + +--- + +### 2. MISSING FIELD: `Subscription.plan` Does Not Exist + +**Problem:** +The `Subscription` model in `auth/models.py` has **NO `plan` field**, but `InvoiceService` and documentation assume it exists. + +**Evidence:** +```python +# Database inspection shows: +class Igny8Subscriptions(models.Model): + tenant = models.OneToOneField('Igny8Tenants') + stripe_subscription_id = CharField + payment_method = CharField + external_payment_id = CharField + status = CharField + current_period_start = DateTimeField + current_period_end = DateTimeField + cancel_at_period_end = BooleanField + # โŒ NO PLAN FIELD +``` + +**Code Expects:** +```python +# In InvoiceService.create_subscription_invoice() +subscription.plan # โŒ AttributeError +``` + +**Impact:** +- Invoice creation for subscriptions **WILL FAIL** +- Cannot determine which plan a subscription belongs to +- Credit allocation logic broken + +**Fix Required:** +Add `plan` field to `Subscription` model: +```python +class Subscription(models.Model): + # ... existing fields ... + plan = models.ForeignKey('igny8_core_auth.Plan', on_delete=models.PROTECT, related_name='subscriptions') +``` + +Migration required to add column and populate from `Account.plan`. + +--- + +### 3. Account.owner Circular Dependency Race Condition + +**Problem:** +Registration creates User and Account in 3 separate steps, causing temporary invalid state: + +```python +# Step 1: User created WITHOUT account +user = User.objects.create_user(account=None) # โš ๏ธ User has no tenant + +# Step 2: Account created WITH user as owner +account = Account.objects.create(owner=user) + +# Step 3: User updated to link to account +user.account = account +user.save() # โš ๏ธ Three DB writes for one logical operation +``` + +**Impact:** +- Between steps 1-3, user exists without `account` (tenant isolation broken) +- If step 2 or 3 fails, orphaned user exists +- Race condition: if another request hits during steps 1-3, middleware fails +- Triple database writes for single logical operation + +**Fix Required:** +Use single transaction or remove `Account.owner` FK entirely and derive from role: + +```python +# Option 1: Single transaction (already wrapped, but still 3 writes) +# Option 2: Remove Account.owner and use property +@property +def owner(self): + return self.users.filter(role='owner').first() +``` + +--- + +### 4. Site.industry Should Be Required But Is Nullable + +**Problem:** +`Site.industry` is `null=True, blank=True` but sector creation **REQUIRES** site to have industry: + +```python +# In Sector.save() line 541 +if self.industry_sector.industry != self.site.industry: + raise ValidationError("Sector must belong to site's industry") +``` + +**Impact:** +- Sites can be created without industry +- When user tries to add sector: **VALIDATION FAILS** (industry is None) +- No sectors can ever be added to sites without industry +- Confusing UX: "Why can't I add sectors?" + +**Evidence:** +```python +# Database schema: +industry = models.ForeignKey(Igny8Industries, blank=True, null=True) # โŒ NULLABLE +``` + +**Fix Required:** +1. Make field required: `Site.industry` โ†’ `null=False, blank=False` +2. Migration: Set default industry for existing NULL sites +3. Update serializers to require industry during site creation + +--- + +### 5. Free Plan Credits Mismatch + +**Problem:** +Documentation says free plan gives 1000 credits, but actual database has only 100: + +**Documentation:** +``` +Free Trial | free | 0.00 | 1000 credits | 1 site | 1 user +``` + +**Actual Database:** +``` +Free Plan | free | 0.00 | 100 credits | 1 site | 1 user +``` + +**Impact:** +- New users get 10x fewer credits than documented +- Sales/marketing materials may be wrong +- User expectations mismatched + +**Fix Required:** +Update Plan record or documentation to match: +```sql +UPDATE igny8_plans SET included_credits = 1000 WHERE slug = 'free'; +``` + +--- + +## ๐ŸŸก MEDIUM PRIORITY ISSUES (Fix Soon) + +### 6. Duplicate Billing Email Fields + +**Problem:** +`billing_email` exists in **TWO** models: + +1. `Account.billing_email` - Primary billing contact +2. `Invoice.billing_email` - Email on invoice (snapshot) + +**Current Code:** +```python +# Account model line 106 +billing_email = models.EmailField(blank=True, null=True) + +# Invoice model line 213 +billing_email = models.EmailField(null=True, blank=True) +``` + +**Impact:** +- Which is source of truth? +- Data can become inconsistent +- Invoice should snapshot billing info at creation time + +**Fix Required:** +- Keep `Account.billing_email` as primary +- Invoice should copy from Account at creation time +- OR: Store full billing snapshot in `Invoice.metadata`: + ```json + { + "billing_snapshot": { + "email": "john@example.com", + "address_line1": "123 Main St", + ... + } + } + ``` + +--- + +### 7. Plan.max_industries Misnamed Field + +**Problem:** +Field is called `max_industries` but controls **sectors per site**, not industries: + +```python +# Plan model line 177 +max_industries = models.IntegerField(help_text="Optional limit for industries/sectors") + +# Site model line 371 +def get_max_sectors_limit(self): + return self.account.plan.max_industries # โŒ Confusing name +``` + +**Evidence from Database:** +``` +Free Plan: max_industries = 1 (means 1 sector per site, NOT 1 industry) +``` + +**Impact:** +- Misleading field name (developers confused) +- Documentation uses "max_industries" and "max_sectors_per_site" inconsistently +- No way to configure unlimited sectors (NULL means fallback to 5) + +**Fix Required:** +1. Rename field: `max_industries` โ†’ `max_sectors_per_site` +2. Migration to rename column +3. Update all references in code +4. Use `0` to mean unlimited + +--- + +### 8. Duplicate Subscription Payment Method + +**Problem:** +Payment method stored in **THREE** places: + +1. `Account.payment_method` - Account default +2. `Subscription.payment_method` - Subscription payment method +3. `AccountPaymentMethod.type` - Saved payment methods + +**Current State:** +```python +# Account line 87 +payment_method = models.CharField(default='stripe') + +# Subscription line 231 +payment_method = models.CharField(default='stripe') + +# AccountPaymentMethod line 476 +type = models.CharField(PAYMENT_METHOD_CHOICES) +``` + +**Impact:** +- Three fields that can be out of sync +- Which one is used for billing? +- AccountPaymentMethod is proper design, others redundant + +**Fix Required:** +- Use `AccountPaymentMethod` as single source of truth +- Deprecate `Account.payment_method` and `Subscription.payment_method` +- Add migration to create AccountPaymentMethod records from existing data + +--- + +### 9. tenant_id vs account Field Name Confusion + +**Problem:** +Django field name is `account`, database column name is `tenant_id`: + +```python +class AccountBaseModel(models.Model): + account = models.ForeignKey(db_column='tenant_id') # โŒ Confusing +``` + +**Impact:** +- ORM uses `account`, raw SQL uses `tenant_id` +- Debugging confusion: "Why isn't `account=1` working in SQL?" +- Must use: `SELECT * FROM igny8_sites WHERE tenant_id=1` (not `account=1`) + +**Fix Required:** +- **Option A:** Rename column to `account_id` (requires migration, data untouched) +- **Option B:** Keep as-is, document clearly (current approach) + +Recommend Option A for consistency. + +--- + +### 10. SiteUserAccess Never Created + +**Problem:** +`SiteUserAccess` model exists for granular site permissions but is **NEVER CREATED**: + +**Expected Flow:** +```python +# During site creation +SiteUserAccess.objects.create(user=owner, site=site) +``` + +**Actual Flow:** +```python +# Site created, NO SiteUserAccess record +site = Site.objects.create(...) # โœ“ Site exists +# โŒ No SiteUserAccess created +``` + +**Impact:** +- Granular site permissions not enforced +- Model exists but unused (dead code) +- User.get_accessible_sites() checks SiteUserAccess but it's always empty +- Only role-based access works (owner/admin see all) + +**Fix Required:** +Auto-create SiteUserAccess on site creation: +```python +# In SiteViewSet.perform_create() +site = serializer.save() +if self.request.user.role in ['owner', 'admin']: + SiteUserAccess.objects.create( + user=self.request.user, + site=site, + granted_by=self.request.user + ) +``` + +--- + +### 11. Credits Auto-Update Missing + +**Problem:** +Account credits manually updated, no service layer: + +```python +# Current approach (scattered throughout codebase) +account.credits += 1000 +account.save() + +# Separate transaction log (can be forgotten) +CreditTransaction.objects.create(...) +``` + +**Impact:** +- Easy to forget logging transaction +- Balance can become inconsistent +- No atomic updates +- No single source of truth + +**Fix Required:** +Create `CreditService` for atomic operations: +```python +class CreditService: + @staticmethod + @transaction.atomic + def add_credits(account, amount, description, metadata=None): + account.credits += amount + account.save() + + CreditTransaction.objects.create( + account=account, + transaction_type='purchase', + amount=amount, + balance_after=account.credits, + description=description, + metadata=metadata or {} + ) + return account.credits +``` + +--- + +## ๐ŸŸข LOW PRIORITY (Technical Debt) + +### 12. Legacy WordPress Fields Unused + +**Problem:** +Site model has 4 WordPress fields but new `SiteIntegration` model exists: + +```python +# Site model (legacy) +wp_url = models.URLField(help_text="legacy - use SiteIntegration") +wp_username = CharField +wp_app_password = CharField +wp_api_key = CharField + +# Newer approach +SiteIntegration.platform = 'wordpress' +SiteIntegration.credentials = {...} +``` + +**Fix Required:** +- Mark fields as deprecated +- Migrate to SiteIntegration +- Remove legacy fields in future release + +--- + +### 13. Plan.credits_per_month Deprecated + +**Problem:** +Two fields for same purpose: + +```python +credits_per_month = IntegerField(default=0) # โŒ Deprecated +included_credits = IntegerField(default=0) # โœ“ Use this + +def get_effective_credits_per_month(self): + return self.included_credits if self.included_credits > 0 else self.credits_per_month +``` + +**Fix Required:** +- Data migration: Copy to `included_credits` +- Remove `credits_per_month` field +- Update method to just return `included_credits` + +--- + +### 14. No Slug Generation Utility + +**Problem:** +Slug generation duplicated in Account, Site, Sector serializers: + +```python +# Duplicated 3+ times: +base_slug = name.lower().replace(' ', '-')[:50] +slug = base_slug +counter = 1 +while Model.objects.filter(slug=slug).exists(): + slug = f"{base_slug}-{counter}" + counter += 1 +``` + +**Fix Required:** +Create utility function: +```python +def generate_unique_slug(model_class, base_name, filters=None, max_length=50): + ... +``` + +--- + +## ๐Ÿ“Š Summary by Severity + +### ๐Ÿ”ด CRITICAL (Blocking - Fix Now) +1. โŒ **billing.Subscription does not exist** - Paid signups FAIL +2. โŒ **Subscription.plan field missing** - Invoice creation broken +3. โš ๏ธ **Account.owner circular dependency** - Race condition risk +4. โš ๏ธ **Site.industry is nullable** - Sector creation fails +5. โš ๏ธ **Free plan credits mismatch** - 100 vs 1000 credits + +**IMMEDIATE ACTION REQUIRED:** +```bash +# Fix #1: Update import in auth/serializers.py line 291 +from igny8_core.auth.models import Subscription + +# Fix #2: Add migration for Subscription.plan field +# Fix #4: Make Site.industry required +# Fix #5: Update Plan.included_credits to 1000 +``` + +--- + +### ๐ŸŸก MEDIUM (Fix This Sprint) +6. Duplicate billing_email fields +7. Plan.max_industries misnamed +8. Duplicate payment_method fields (3 places) +9. tenant_id vs account naming confusion +10. SiteUserAccess never created +11. No CreditService for atomic updates + +--- + +### ๐ŸŸข LOW (Technical Debt) +12. Legacy WordPress fields +13. Plan.credits_per_month deprecated +14. No slug generation utility + +--- + +## ๐Ÿ”ง Required Actions Before Production + +### Phase 1: Emergency Fixes (Today) + +```python +# 1. Fix Subscription import +# File: backend/igny8_core/auth/serializers.py line 291 +from igny8_core.auth.models import Subscription # Changed from billing.models + +# 2. Add Subscription.plan field +# New migration: +class Migration: + operations = [ + 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 for data migration + ), + ), + # Data migration: Copy plan from account + migrations.RunPython(copy_plan_from_account), + # Make non-nullable + migrations.AlterField( + model_name='subscription', + name='plan', + field=models.ForeignKey( + 'igny8_core_auth.Plan', + on_delete=models.PROTECT, + related_name='subscriptions' + ), + ), + ] +``` + +### Phase 2: Data Integrity (This Week) + +```sql +-- Fix free plan credits +UPDATE igny8_plans SET included_credits = 1000 WHERE slug = 'free'; + +-- Make Site.industry required (after setting defaults) +UPDATE igny8_sites SET industry_id = 2 WHERE industry_id IS NULL; -- Technology +ALTER TABLE igny8_sites ALTER COLUMN industry_id SET NOT NULL; +``` + +### Phase 3: Architecture Improvements (Next Sprint) + +1. Create CreditService +2. Auto-create SiteUserAccess +3. Rename max_industries โ†’ max_sectors_per_site +4. Consolidate payment method fields + +--- + +## ๐Ÿงช Testing Required After Fixes + +### Test 1: Free Trial Signup +```bash +POST /api/v1/auth/register/ +{ + "email": "test@example.com", + "password": "Test123!", + "password_confirm": "Test123!", + "account_name": "Test Account", + "plan_slug": "free" +} + +# Expected: +# โœ“ User created +# โœ“ Account created with 1000 credits (not 100) +# โœ“ CreditTransaction logged +# โœ“ No errors +``` + +### Test 2: Paid Plan Signup +```bash +POST /api/v1/auth/register/ +{ + "email": "paid@example.com", + "password": "Test123!", + "password_confirm": "Test123!", + "account_name": "Paid Account", + "plan_slug": "starter", + "payment_method": "bank_transfer" +} + +# Expected: +# โœ“ User created +# โœ“ Account created with status='pending_payment' +# โœ“ Subscription created with plan FK +# โœ“ Invoice created +# โœ“ AccountPaymentMethod created +# โœ“ No errors (currently FAILS) +``` + +### Test 3: Site Creation +```bash +POST /api/v1/auth/sites/ +{ + "name": "Test Site", + "domain": "https://test.com", + "industry": 2 # Must be required +} + +# Expected: +# โœ“ Site created +# โœ“ SiteUserAccess created for owner +# โœ“ Can add sectors +``` + +### Test 4: Sector Creation +```bash +POST /api/v1/auth/sectors/ +{ + "site": 1, + "name": "Web Development", + "industry_sector": 4 +} + +# Expected: +# โœ“ Sector created +# โœ“ Validation: industry_sector.industry == site.industry +# โœ“ Validation: sector count < plan.max_sectors_per_site +``` + +--- + +## ๐Ÿ“ Conclusion + +**Current State:** Registration is **PARTIALLY BROKEN** +- โœ… Free trial signups work (with credit amount issue) +- โŒ Paid plan signups completely broken +- โš ๏ธ Site/sector creation has validation issues +- โš ๏ธ Data integrity risks from duplicate fields + +**Estimated Fix Time:** +- Critical fixes: 2-4 hours +- Medium fixes: 1-2 days +- Low priority: 1 week + +**Recommended Approach:** +1. Fix critical import and field issues (Phase 1) - **URGENT** +2. Test all signup flows thoroughly +3. Address medium priority issues incrementally +4. Plan technical debt cleanup for next quarter + +--- + +**Document Version:** 1.0 +**Next Review:** After Phase 1 fixes implemented +**Owner:** Development Team diff --git a/IMPLEMENTATION-PLAN-SIGNUP-TO-PAYMENT-WORKFLOW.md b/IMPLEMENTATION-PLAN-SIGNUP-TO-PAYMENT-WORKFLOW.md new file mode 100644 index 00000000..4996eb13 --- /dev/null +++ b/IMPLEMENTATION-PLAN-SIGNUP-TO-PAYMENT-WORKFLOW.md @@ -0,0 +1,2488 @@ +# IMPLEMENTATION PLAN: Clean Signup to Payment Workflow +**Version:** 2.0 +**Date:** December 8, 2025 +**Status:** ๐Ÿ”ง READY FOR IMPLEMENTATION + +--- + +## Table of Contents +1. [Executive Summary](#executive-summary) +2. [Deep Analysis Findings](#deep-analysis-findings) +3. [Field & Relationship Audit](#field--relationship-audit) +4. [Simplified Model Architecture](#simplified-model-architecture) +5. [Complete Workflow Diagrams](#complete-workflow-diagrams) +6. [Implementation Phases](#implementation-phases) +7. [Database Migrations](#database-migrations) +8. [Testing Plan](#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` + +**Complexity Issues:** +- 4 global payment methods + 1 country-specific = unnecessary complexity +- 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 + +### 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 (3):** Stripe, PayPal, Bank Transfer +- **Country-Specific (1):** Local Wallet (Pakistan only) +- **Total:** 4 payment methods (removed unnecessary complexity) + +--- + +## Deep Analysis Findings + +### 1. Date Field Redundancy + +**Problem:** Period dates duplicated in multiple places + +**Current State:** +```python +# 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:** +```python +# 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:** +```python +# 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:** +```python +# 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:** +```python +# 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:** +```python +# 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:** +```python +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:** +```python +# 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:** +```python +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:** +```python +# 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:** +```python +# 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:** +```python +# 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:** +```python +# 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:** +```python +# 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:** +```python +# 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:** +```python +# 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:** +```python +# auth/serializers.py line 291 +from igny8_core.business.billing.models import Subscription # โŒ DOES NOT EXIST +``` + +**Fix:** +```python +# Use existing auth.Subscription +from igny8_core.auth.models import Subscription # โœ“ EXISTS +``` + +--- + +## Field & Relationship Audit + +### Complete Field Inventory + +#### Subscription Model - BEFORE Cleanup +```python +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 +```python +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 +```python +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 +```python +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:** +```sql +-- 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):** +```sql +-- 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:** +```sql +-- 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:** +```sql +-- 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:** +```sql +-- 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 +```python +# 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 +```bash +cd backend +python manage.py makemigrations --name add_subscription_plan_field + +# Migration file created: 0XXX_add_subscription_plan_field.py +``` + +```python +# 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' + ), + ), + ] +``` + +```bash +# Apply migration +python manage.py migrate +``` + +#### 1.3 Make Site.industry Required +```bash +python manage.py makemigrations --name make_site_industry_required +``` + +```python +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 +```bash +docker exec igny8_backend python manage.py shell +``` + +```python +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:** +```bash +# 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 +```bash +python manage.py makemigrations --name remove_duplicate_invoice_fields +``` + +```python +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 +```python +# 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 +```bash +python manage.py makemigrations --name remove_subscription_payment_method +``` + +```python +class Migration(migrations.Migration): + operations = [ + migrations.RemoveField('subscription', 'payment_method'), + ] +``` + +#### 2.4 Add payment_method Property to Subscription +```python +# 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 +```python +# 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 +```bash +python manage.py makemigrations --name remove_duplicate_payment_reference +``` + +```python +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 +```bash +docker exec igny8_backend python manage.py shell +``` + +```python +from igny8_core.business.billing.models import PaymentMethodConfig + +# Global methods (available everywhere) +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 + } +) + +PaymentMethodConfig.objects.get_or_create( + country_code='*', + 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: ABCPKKA + + Please transfer the exact invoice amount and keep the transaction reference. + ''', + 'sort_order': 3 + } +) + +# Pakistan-specific +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 Name: IGNY8 + + Please keep the transaction ID and confirm payment after sending. + ''', + 'sort_order': 4 + } +) + +print("Payment method configurations created!") +``` + +#### 3.2 Create Payment Methods API Endpoint +```python +# 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 +```python +# 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 + ) +``` + + +#### 3.4 Update RegisterSerializer for Billing Fields +```python +# File: backend/igny8_core/auth/serializers.py + +class RegisterSerializer(serializers.ModelSerializer): + # ... existing fields ... + + # Add billing fields + billing_email = serializers.EmailField(required=False, allow_blank=True) + billing_address = serializers.CharField(required=False, allow_blank=True) + billing_city = serializers.CharField(required=False, allow_blank=True) + billing_country = serializers.CharField(required=False, allow_blank=True) + payment_method = serializers.CharField(required=False, default='stripe') + + class Meta: + model = User + fields = [ + 'email', 'password', 'password_confirm', 'first_name', 'last_name', + 'billing_email', 'billing_address', 'billing_city', 'billing_country', + 'payment_method', 'plan_slug' + ] + + def create(self, validated_data): + # ... existing account/user creation ... + + # Update billing fields if provided + billing_email = validated_data.pop('billing_email', None) + billing_address = validated_data.pop('billing_address', None) + billing_city = validated_data.pop('billing_city', None) + billing_country = validated_data.pop('billing_country', None) + payment_method = validated_data.pop('payment_method', 'stripe') + + if billing_email: + account.billing_email = billing_email + if billing_address: + account.billing_address = billing_address + if billing_city: + account.billing_city = billing_city + if billing_country: + account.billing_country = billing_country + + account.save(update_fields=[ + 'billing_email', 'billing_address', 'billing_city', 'billing_country' + ]) + + # Create AccountPaymentMethod if not free trial + if not is_free_trial: + from igny8_core.business.billing.models import AccountPaymentMethod + AccountPaymentMethod.objects.create( + account=account, + type=payment_method, + is_default=True, + is_enabled=True + ) + + # ... rest of creation logic ... +``` + +#### 3.5 Frontend: Add Billing Form Step +```typescript +// File: frontend/src/components/auth/SignUpForm.tsx + +interface BillingFormData { + billing_email: string; + billing_address: string; + billing_city: string; + billing_country: string; + payment_method: string; +} + +const [step, setStep] = useState<'signup' | 'billing'>('signup'); +const [billingData, setBillingData] = useState({ + billing_email: '', + billing_address: '', + billing_city: '', + billing_country: '', + payment_method: 'stripe' +}); + +// After initial form submission +const handleSignupSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + // If paid plan, show billing form + if (planSlug && planSlug !== 'free') { + setStep('billing'); + } else { + // Free trial, submit directly + await submitRegistration(); + } +}; + +// Billing form submission +const handleBillingSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + await submitRegistration(); +}; + +const submitRegistration = async () => { + const payload = { + email, + password, + password_confirm: passwordConfirm, + first_name: firstName, + last_name: lastName, + plan_slug: planSlug, + ...(step === 'billing' ? billingData : {}) + }; + + await register(payload); +}; + +// Render billing form +{step === 'billing' && ( +
+

Billing Information

+ + setBillingData({ + ...billingData, + billing_email: e.target.value + })} + required + /> + + setBillingData({ + ...billingData, + billing_address: e.target.value + })} + required + /> + + setBillingData({ + ...billingData, + billing_country: country + })} + /> + + setBillingData({ + ...billingData, + payment_method: method + })} + /> + + +
+)} +``` + +#### 3.6 Admin Payment Approval Interface +```python +# 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 +```bash +# 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:** +```sql +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 +```bash +# 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:** +```bash +# 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 " \ + -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 +```bash +# After account activated, create site +curl -X POST http://localhost:8000/api/v1/auth/sites/ \ + -H "Authorization: Bearer " \ + -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 " \ + -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 +```bash +# 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 +```python +# Subscription ++ plan = FK(Plan) # NEW +- payment_method # REMOVE (use property) + +# Invoice +- 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 +- transaction_reference # REMOVE (duplicate of manual_reference) + +# Site +! industry = FK(Industry, null=False) # MAKE REQUIRED + +# Account (no changes, keep payment_method for backward compatibility) +``` + +### API Endpoints Created +``` +GET /v1/billing/payment-methods/?country=PK +POST /v1/billing/payments/confirm/ +POST /v1/auth/register/ (enhanced with billing fields) +``` + +### Database Queries for Verification +```sql +-- 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) +``` +