# 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) ```