Files
igny8/IMPLEMENTATION-PLAN-SIGNUP-TO-PAYMENT-WORKFLOW.md
alorig 92d16c76a7 Revert "sadasd"
This reverts commit 9f85ce4f52.
2025-12-09 00:26:01 +05:00

2489 lines
109 KiB
Markdown

# 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<BillingFormData>({
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' && (
<div className="billing-form">
<h3>Billing Information</h3>
<Input
label="Billing Email"
type="email"
value={billingData.billing_email}
onChange={(e) => setBillingData({
...billingData,
billing_email: e.target.value
})}
required
/>
<Input
label="Address"
value={billingData.billing_address}
onChange={(e) => setBillingData({
...billingData,
billing_address: e.target.value
})}
required
/>
<CountrySelect
label="Country"
value={billingData.billing_country}
onChange={(country) => setBillingData({
...billingData,
billing_country: country
})}
/>
<PaymentMethodSelect
country={billingData.billing_country}
value={billingData.payment_method}
onChange={(method) => setBillingData({
...billingData,
payment_method: method
})}
/>
<Button onClick={handleBillingSubmit}>
Complete Signup
</Button>
</div>
)}
```
#### 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 <token>" \
-H "Content-Type: application/json" \
-d '{
"invoice_id": 2,
"manual_reference": "BT-20251208-12345",
"manual_notes": "Transferred via ABC Bank on Dec 8"
}'
# Expected:
# {
# "success": true,
# "message": "Payment confirmation submitted for review",
# "data": {"payment_id": 1, "status": "pending_approval"}
# }
```
#### 4.3 Test: Site Creation with Industry
```bash
# After account activated, create site
curl -X POST http://localhost:8000/api/v1/auth/sites/ \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"name": "My Tech Blog",
"domain": "https://mytechblog.com",
"industry": 2,
"site_type": "blog"
}'
# Expected: ✅ Site created
# Test without industry (should fail)
curl -X POST http://localhost:8000/api/v1/auth/sites/ \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"name": "My Blog",
"domain": "https://myblog.com"
}'
# Expected: ❌ 400 Bad Request, "industry is required"
```
#### 4.4 Test: Payment Method Filtering by Country
```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)
```