Files
igny8/docs/plans/implemented/payment-refactor-plan.md
IGNY8 VPS (Salman) cf5d4b5b37 reorg
2026-01-09 16:49:19 +00:00

76 KiB

Payment System Complete Refactor Plan

Version: 1.0 Date: January 7, 2026 Status: In Progress - Phase 1 Complete


Executive Summary

This plan consolidates all audit findings and architectural discussions into a comprehensive refactor that will:

  1. Simplify signup flow - Remove payment gateway redirect from signup
  2. Unify payment experience - Single payment interface in /account/plans
  3. Fix all critical security issues - Webhook verification, idempotency
  4. Clean up dead code - Remove /pk variant, country detection API, unused fields
  5. Implement proper state handling - Account lifecycle properly reflected in UI

Architecture Changes

Before (Current)

┌─────────────────────────────────────────────────────────────────┐
│                      CURRENT FLOW                                │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  /signup OR /signup/pk                                          │
│       │                                                          │
│       ├── Country detection API call                            │
│       ├── Load payment methods API call                         │
│       ├── Show payment method selection                         │
│       │                                                          │
│       └── On Submit:                                            │
│           ├── Stripe → Redirect to Stripe → Return to /plans    │
│           ├── PayPal → Redirect to PayPal → Return to /plans    │
│           └── Bank → Navigate to /plans (show banner)           │
│                                                                  │
│  PROBLEMS:                                                       │
│  - Two signup routes (/signup and /signup/pk)                   │
│  - Country detection API complexity                              │
│  - Payment gateway redirect from signup                          │
│  - User loses context if redirect fails                          │
│  - Duplicate payment logic (signup + plans page)                │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

After (New)

┌─────────────────────────────────────────────────────────────────┐
│                       NEW FLOW                                   │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  /signup (SINGLE ROUTE)                                         │
│       │                                                          │
│       ├── Country dropdown (auto-detect default using IP API)   │
│       ├── Plan selection                                        │
│       ├── NO payment method selection                           │
│       │                                                          │
│       └── On Submit:                                            │
│           └── Create account (pending_payment)                  │
│           └── Navigate to /account/plans                        │
│                                                                  │
│  /account/plans                                                 │
│       │                                                          │
│       ├── IF new user (never paid):                             │
│       │   └── Show PendingPaymentView (full page)               │
│       │       - Invoice details                                  │
│       │       - Payment method selection (based on country)      │
│       │       - Inline payment form                              │
│       │                                                          │
│       ├── IF existing user (has paid before):                   │
│       │   └── Show ActiveSubscriptionView                       │
│       │       - Current plan details                             │
│       │       - Credit balance                                   │
│       │       - Buy credits (modal)                              │
│       │       - Billing history                                  │
│       │       - IF pending invoice: Show banner + Pay modal      │
│       │                                                          │
└─────────────────────────────────────────────────────────────────┘

Phase 1: Backend Cleanup & Security Fixes

1.1 Fix Critical Security Issues

1.1.1 Enable PayPal Webhook Signature Verification

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

# Line ~510 - UNCOMMENT the rejection
if not is_valid:
    logger.error("PayPal webhook signature verification failed")
    return Response({'error': 'Invalid signature'}, status=400)  # UNCOMMENT THIS

1.1.2 Add Stripe Webhook Idempotency

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

# Add at start of _handle_checkout_completed (around line 380):
@transaction.atomic
def _handle_checkout_completed(session):
    session_id = session.get('id')

    # IDEMPOTENCY CHECK
    if Payment.objects.filter(
        metadata__stripe_checkout_session_id=session_id
    ).exists():
        logger.info(f"Webhook already processed for session {session_id}")
        return

    # ... rest of handler

1.1.3 Add PayPal Capture Idempotency

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

# Add at start of PayPalCaptureOrderView.post (around line 270):
def post(self, request):
    order_id = request.data.get('order_id')

    # IDEMPOTENCY CHECK
    existing = Payment.objects.filter(
        paypal_order_id=order_id,
        status='succeeded'
    ).first()
    if existing:
        return Response({
            'status': 'already_captured',
            'payment_id': existing.id,
            'message': 'This order has already been captured'
        })

    # ... rest of handler

1.1.4 Add PayPal Amount Validation

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

# In _process_credit_purchase (around line 570):
captured_amount = Decimal(str(capture_result.get('amount', '0')))
expected_amount = Decimal(str(package.price))

if abs(captured_amount - expected_amount) > Decimal('0.01'):
    logger.error(f"Amount mismatch: captured={captured_amount}, expected={expected_amount}")
    return Response({
        'error': 'Payment amount does not match expected amount',
        'captured': str(captured_amount),
        'expected': str(expected_amount)
    }, status=400)

1.1.5 Fix Refund Module Imports

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

# Replace non-existent imports (around line 160):
# FROM:
from igny8_core.business.billing.utils.payment_gateways import get_stripe_client
from igny8_core.business.billing.utils.payment_gateways import get_paypal_client

# TO:
from igny8_core.business.billing.services.stripe_service import StripeService
from igny8_core.business.billing.services.paypal_service import PayPalService

# Then use:
stripe_service = StripeService()
stripe_service.create_refund(payment_intent_id, amount)

1.2 Simplify Registration Serializer

File: backend/igny8_core/auth/serializers.py

REMOVE from RegisterSerializer.create():

  • Stripe checkout session creation
  • PayPal order creation
  • checkout_url return
  • checkout_session_id return
  • paypal_order_id return

KEEP:

  • User creation
  • Account creation with status='pending_payment'
  • Subscription creation with status='pending_payment'
  • Invoice creation
  • AccountPaymentMethod creation (just store type, don't verify)

NEW create() method (simplified):

def create(self, validated_data):
    # Extract fields
    plan_slug = validated_data.pop('plan_slug', 'free')
    billing_country = validated_data.pop('billing_country', '')

    # Create user
    user = User.objects.create_user(...)

    # Get plan
    plan = Plan.objects.get(slug=plan_slug, is_active=True)
    is_paid_plan = plan.price > 0

    # Create account
    account = Account.objects.create(
        name=validated_data.get('account_name', user.username),
        owner=user,
        plan=plan,
        credits=0 if is_paid_plan else plan.included_credits,
        status='pending_payment' if is_paid_plan else 'active',
        billing_country=billing_country,
    )

    if is_paid_plan:
        # Create subscription
        subscription = Subscription.objects.create(
            account=account,
            plan=plan,
            status='pending_payment',
            current_period_start=timezone.now(),
            current_period_end=timezone.now() + timedelta(days=30),
        )

        # Create invoice
        InvoiceService.create_subscription_invoice(
            subscription=subscription,
            payment_method=None,  # Will be set when user pays
        )

    return user  # NO checkout_url returned

File: backend/igny8_core/auth/views.py

REMOVE from RegisterView.post():

  • Stripe checkout session creation
  • PayPal order creation
  • checkout_url in response

NEW response (simplified):

def post(self, request):
    serializer = RegisterSerializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    user = serializer.save()

    # Generate tokens
    tokens = get_tokens_for_user(user)

    return Response({
        'success': True,
        'user': UserSerializer(user).data,
        'tokens': tokens,
        # NO checkout_url - user will pay on /account/plans
    })

1.3 Add Country List Endpoint

File: backend/igny8_core/auth/urls.py (NEW)

class CountryListView(APIView):
    """Returns list of countries for signup dropdown"""
    permission_classes = []  # Public endpoint

    def get(self, request):
        countries = [
            {'code': 'US', 'name': 'United States'},
            {'code': 'GB', 'name': 'United Kingdom'},
            {'code': 'CA', 'name': 'Canada'},
            {'code': 'AU', 'name': 'Australia'},
            {'code': 'PK', 'name': 'Pakistan'},
            {'code': 'IN', 'name': 'India'},
            # ... full list
        ]
        return Response({'countries': countries})

File: backend/igny8_core/auth/urls.py

path('countries/', CountryListView.as_view(), name='country-list'),

1.4 Add WebhookEvent Model for Audit Trail

File: backend/igny8_core/business/billing/models.py (ADD)

class WebhookEvent(models.Model):
    """Store all incoming webhook events for audit and replay"""
    event_id = models.CharField(max_length=255, unique=True)
    provider = models.CharField(max_length=20)  # 'stripe' or 'paypal'
    event_type = models.CharField(max_length=100)
    payload = models.JSONField()
    processed = models.BooleanField(default=False)
    processed_at = models.DateTimeField(null=True, blank=True)
    error_message = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        indexes = [
            models.Index(fields=['provider', 'event_type']),
            models.Index(fields=['processed']),
        ]

1.5 Add Invoice Unique Constraint

Migration: Add unique constraint to invoice_number

# In Invoice model
invoice_number = models.CharField(max_length=50, unique=True)  # Already has unique=True

Update invoice_service.py to handle IntegrityError:

from django.db import IntegrityError

@transaction.atomic
def generate_invoice_number(account_id, invoice_type='SUB'):
    prefix = f"INV-{account_id}-{invoice_type}-{timezone.now().strftime('%Y%m')}"

    for attempt in range(5):
        count = Invoice.objects.filter(
            invoice_number__startswith=prefix
        ).count()
        invoice_number = f"{prefix}-{count + 1:04d}"

        # Check if exists (within transaction)
        if not Invoice.objects.filter(invoice_number=invoice_number).exists():
            return invoice_number

    raise ValueError(f"Unable to generate unique invoice number after 5 attempts")

1.6 Add Manual Reference Uniqueness

Migration:

class Migration(migrations.Migration):
    operations = [
        migrations.AlterField(
            model_name='payment',
            name='manual_reference',
            field=models.CharField(
                max_length=255,
                blank=True,
                null=True,
                unique=True,  # ADD UNIQUE
            ),
        ),
    ]

Phase 2: Frontend Cleanup

2.1 Simplify SignUpFormUnified.tsx

File: frontend/src/components/auth/SignUpFormUnified.tsx

REMOVE:

  • Payment method selection UI
  • Payment method loading from API (/v1/billing/payment-configs/payment-methods/)
  • Stripe checkout redirect logic
  • PayPal redirect logic
  • isPakistanSignup variant logic
  • All payment gateway-related state

KEEP:

  • Email, password, name fields
  • Plan selection
  • Country dropdown (NEW - replaces detection)

ADD:

  • Country dropdown with auto-detection default
  • Simplified submit that only creates account

NEW component structure:

export default function SignUpFormUnified() {
  // State
  const [formData, setFormData] = useState({
    email: '',
    password: '',
    username: '',
    first_name: '',
    last_name: '',
    account_name: '',
    billing_country: '', // From dropdown
  });
  const [selectedPlan, setSelectedPlan] = useState<Plan | null>(null);
  const [countries, setCountries] = useState<Country[]>([]);
  const [detectedCountry, setDetectedCountry] = useState('US');

  // Load countries and detect user's country
  useEffect(() => {
    loadCountries();
    detectUserCountry();
  }, []);

  const loadCountries = async () => {
    const response = await fetch(`${API_BASE_URL}/v1/auth/countries/`);
    const data = await response.json();
    setCountries(data.countries);
  };

  const detectUserCountry = async () => {
    try {
      // Use IP-based detection (optional - fallback to US)
      const response = await fetch('https://ipapi.co/json/');
      const data = await response.json();
      setDetectedCountry(data.country_code || 'US');
      setFormData(prev => ({ ...prev, billing_country: data.country_code || 'US' }));
    } catch {
      setDetectedCountry('US');
      setFormData(prev => ({ ...prev, billing_country: 'US' }));
    }
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);

    try {
      // Register - NO payment redirect
      const result = await register({
        ...formData,
        plan_slug: selectedPlan?.slug || 'free',
      });

      // Always navigate to plans page
      // For paid plans: will show PendingPaymentView
      // For free plan: will show ActiveSubscriptionView
      if (selectedPlan && selectedPlan.price > 0) {
        navigate('/account/plans');
        toast.info('Complete your payment to activate your plan');
      } else {
        navigate('/dashboard');
        toast.success('Welcome to IGNY8!');
      }
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* Email, Password, Name fields */}

      {/* Country Dropdown - NEW */}
      <Select
        label="Country"
        value={formData.billing_country}
        onChange={(value) => setFormData({ ...formData, billing_country: value })}
        options={countries.map(c => ({ value: c.code, label: c.name }))}
      />

      {/* Plan Selection */}
      <PlanSelector
        plans={plans}
        selectedPlan={selectedPlan}
        onSelect={setSelectedPlan}
      />

      {/* Submit Button - Different text based on plan */}
      <Button type="submit" loading={loading}>
        {selectedPlan?.price > 0 ? 'Continue to Payment' : 'Create Free Account'}
      </Button>
    </form>
  );
}

2.2 Remove /signup/pk Route

File: frontend/src/routes/index.tsx (or equivalent)

REMOVE:

// DELETE THIS ROUTE
<Route path="/signup/pk" element={<SignupPage isPakistan />} />

KEEP only:

<Route path="/signup" element={<SignupPage />} />

2.3 Remove Country Detection API Usage

Files to update:

  • frontend/src/components/auth/SignUpFormUnified.tsx - Remove ipapi.co calls for complex detection
  • frontend/src/services/billing.api.ts - Remove detectCountry() function if exists

NEW approach: Simple IP-based detection for dropdown default only:

// Simple one-liner for default selection
const detectCountry = async () => {
  try {
    const res = await fetch('https://ipapi.co/country_code/');
    return await res.text();
  } catch {
    return 'US';
  }
};

2.4 Refactor PlansAndBillingPage.tsx

File: frontend/src/pages/account/PlansAndBillingPage.tsx

NEW structure:

export default function PlansAndBillingPage() {
  const { user, refreshUser } = useAuthStore();
  const [loading, setLoading] = useState(true);
  const [invoices, setInvoices] = useState<Invoice[]>([]);
  const [payments, setPayments] = useState<Payment[]>([]);

  const accountStatus = user?.account?.status;
  const billingCountry = user?.account?.billing_country;

  // Determine if user has ever paid
  const hasEverPaid = payments.some(p => p.status === 'succeeded');

  // Determine view mode
  const isNewUserPendingPayment = accountStatus === 'pending_payment' && !hasEverPaid;

  useEffect(() => {
    loadBillingData();
  }, []);

  if (loading) return <LoadingSpinner />;

  // NEW USER - Show full-page payment view
  if (isNewUserPendingPayment) {
    return (
      <PendingPaymentView
        invoice={invoices.find(i => i.status === 'pending')}
        userCountry={billingCountry}
        onPaymentSuccess={() => {
          refreshUser();
          loadBillingData();
        }}
      />
    );
  }

  // EXISTING USER - Show normal billing dashboard
  return (
    <ActiveSubscriptionView
      user={user}
      invoices={invoices}
      payments={payments}
      hasPendingInvoice={invoices.some(i => i.status === 'pending')}
      onRefresh={loadBillingData}
    />
  );
}

2.5 Create PendingPaymentView Component

File: frontend/src/components/billing/PendingPaymentView.tsx (NEW)

interface PendingPaymentViewProps {
  invoice: Invoice | null;
  userCountry: string;
  onPaymentSuccess: () => void;
}

export default function PendingPaymentView({
  invoice,
  userCountry,
  onPaymentSuccess,
}: PendingPaymentViewProps) {
  const isPakistan = userCountry?.toUpperCase() === 'PK';

  // Payment methods based on country
  const availableMethods: PaymentMethod[] = isPakistan
    ? ['stripe', 'bank_transfer']
    : ['stripe', 'paypal'];

  const [selectedMethod, setSelectedMethod] = useState<PaymentMethod>(availableMethods[0]);
  const [loading, setLoading] = useState(false);
  const [bankDetails, setBankDetails] = useState<BankDetails | null>(null);

  // Load bank details for PK users
  useEffect(() => {
    if (isPakistan) {
      loadBankDetails();
    }
  }, [isPakistan]);

  const loadBankDetails = async () => {
    const response = await fetch(
      `${API_BASE_URL}/v1/billing/payment-configs/payment-methods/?country_code=PK&payment_method=bank_transfer`
    );
    const data = await response.json();
    if (data.results?.length > 0) {
      setBankDetails(data.results[0]);
    }
  };

  const handleStripePayment = async () => {
    setLoading(true);
    try {
      const result = await subscribeToPlan(invoice.subscription.plan.slug, 'stripe', {
        return_url: `${window.location.origin}/account/plans?success=true`,
        cancel_url: `${window.location.origin}/account/plans?canceled=true`,
      });
      window.location.href = result.redirect_url;
    } catch (err) {
      toast.error(err.message);
    } finally {
      setLoading(false);
    }
  };

  const handlePayPalPayment = async () => {
    setLoading(true);
    try {
      const result = await subscribeToPlan(invoice.subscription.plan.slug, 'paypal', {
        return_url: `${window.location.origin}/account/plans?paypal=success`,
        cancel_url: `${window.location.origin}/account/plans?paypal=cancel`,
      });
      window.location.href = result.redirect_url;
    } catch (err) {
      toast.error(err.message);
    } finally {
      setLoading(false);
    }
  };

  const handleBankTransferSubmit = async (formData: BankTransferFormData) => {
    setLoading(true);
    try {
      await submitManualPayment({
        invoice_id: invoice.id,
        payment_method: 'bank_transfer',
        amount: invoice.total_amount,
        manual_reference: formData.reference,
        manual_notes: formData.notes,
        proof_url: formData.proofUrl,
      });
      toast.success('Payment submitted for approval');
      onPaymentSuccess();
    } catch (err) {
      toast.error(err.message);
    } finally {
      setLoading(false);
    }
  };

  if (!invoice) {
    return <div>No pending invoice found</div>;
  }

  return (
    <div className="max-w-2xl mx-auto py-12 px-4">
      {/* Header */}
      <div className="text-center mb-8">
        <h1 className="text-3xl font-bold text-gray-900 dark:text-white">
          Complete Your Subscription
        </h1>
        <p className="mt-2 text-gray-600 dark:text-gray-400">
          Your {invoice.subscription?.plan?.name} plan is ready to activate
        </p>
      </div>

      {/* Invoice Summary Card */}
      <Card className="mb-6">
        <div className="flex justify-between items-center">
          <div>
            <p className="text-sm text-gray-500">Invoice</p>
            <p className="font-medium">#{invoice.invoice_number}</p>
          </div>
          <div className="text-right">
            <p className="text-sm text-gray-500">Amount Due</p>
            <p className="text-2xl font-bold">
              {invoice.currency} {invoice.total_amount}
            </p>
          </div>
        </div>
        <div className="mt-4 pt-4 border-t">
          <div className="flex justify-between text-sm">
            <span className="text-gray-500">Plan</span>
            <span>{invoice.subscription?.plan?.name}</span>
          </div>
          <div className="flex justify-between text-sm mt-1">
            <span className="text-gray-500">Billing Period</span>
            <span>Monthly</span>
          </div>
        </div>
      </Card>

      {/* Payment Method Selection */}
      <Card className="mb-6">
        <h3 className="font-semibold mb-4">Select Payment Method</h3>
        <div className="flex gap-3">
          {availableMethods.map((method) => (
            <button
              key={method}
              onClick={() => setSelectedMethod(method)}
              className={`flex-1 p-4 rounded-lg border-2 transition-all ${
                selectedMethod === method
                  ? 'border-brand-500 bg-brand-50'
                  : 'border-gray-200 hover:border-gray-300'
              }`}
            >
              {method === 'stripe' && <CreditCardIcon className="w-6 h-6 mx-auto mb-2" />}
              {method === 'paypal' && <WalletIcon className="w-6 h-6 mx-auto mb-2" />}
              {method === 'bank_transfer' && <Building2Icon className="w-6 h-6 mx-auto mb-2" />}
              <span className="block text-sm font-medium">
                {method === 'stripe' && 'Credit/Debit Card'}
                {method === 'paypal' && 'PayPal'}
                {method === 'bank_transfer' && 'Bank Transfer'}
              </span>
            </button>
          ))}
        </div>
      </Card>

      {/* Payment Form */}
      <Card>
        {selectedMethod === 'stripe' && (
          <div className="text-center py-6">
            <CreditCardIcon className="w-12 h-12 mx-auto text-brand-500 mb-4" />
            <p className="text-gray-600 mb-4">
              Pay securely with your credit or debit card via Stripe
            </p>
            <Button
              variant="primary"
              onClick={handleStripePayment}
              loading={loading}
              className="w-full"
            >
              Pay {invoice.currency} {invoice.total_amount}
            </Button>
          </div>
        )}

        {selectedMethod === 'paypal' && (
          <div className="text-center py-6">
            <WalletIcon className="w-12 h-12 mx-auto text-[#0070ba] mb-4" />
            <p className="text-gray-600 mb-4">
              Pay securely with your PayPal account
            </p>
            <Button
              variant="primary"
              onClick={handlePayPalPayment}
              loading={loading}
              className="w-full bg-[#0070ba] hover:bg-[#005ea6]"
            >
              Pay with PayPal
            </Button>
          </div>
        )}

        {selectedMethod === 'bank_transfer' && (
          <BankTransferForm
            bankDetails={bankDetails}
            invoiceNumber={invoice.invoice_number}
            amount={`${invoice.currency} ${invoice.total_amount}`}
            onSubmit={handleBankTransferSubmit}
            loading={loading}
          />
        )}
      </Card>

      {/* Change Plan Link */}
      <div className="mt-6 text-center">
        <button
          onClick={() => setShowPlanSelector(true)}
          className="text-sm text-gray-500 hover:text-gray-700"
        >
          Need a different plan?
        </button>
      </div>
    </div>
  );
}

2.6 Create ActiveSubscriptionView Component

Status: Existing PlansAndBillingPage.tsx serves this purpose - no separate component needed. The current implementation now conditionally renders PendingPaymentView for new users, and the full billing dashboard (effectively "ActiveSubscriptionView") for existing users.

File: frontend/src/pages/account/views/ActiveSubscriptionView.tsx (NEW)

This is the cleaned-up version of current PlansAndBillingPage with:

  • Proper state handling
  • Correct badge colors
  • Disabled buttons based on state
  • Payment modal only for credits and overdue invoices
export default function ActiveSubscriptionView({
  user,
  invoices,
  payments,
  hasPendingInvoice,
  onRefresh,
}: ActiveSubscriptionViewProps) {
  const accountStatus = user?.account?.status;
  const subscription = user?.account?.subscription;

  // Get subscription display
  const getSubscriptionDisplay = () => {
    if (!subscription) return { label: 'No Plan', tone: 'error' as const };

    switch (subscription.status) {
      case 'active':
        return hasPendingInvoice
          ? { label: 'Payment Due', tone: 'warning' as const }
          : { label: 'Active', tone: 'success' as const };
      case 'pending_payment':
        return { label: 'Awaiting Payment', tone: 'warning' as const };
      case 'past_due':
        return { label: 'Payment Overdue', tone: 'error' as const };
      case 'canceled':
        return { label: 'Cancels Soon', tone: 'warning' as const };
      default:
        return { label: subscription.status, tone: 'neutral' as const };
    }
  };

  const statusDisplay = getSubscriptionDisplay();

  // Can user perform actions?
  const canUpgrade = accountStatus === 'active' && !hasPendingInvoice;
  const canBuyCredits = accountStatus === 'active' && !hasPendingInvoice;
  const canCancel = accountStatus === 'active' && !hasPendingInvoice;
  const canManageBilling = user?.account?.stripe_customer_id && canUpgrade;

  return (
    <div className="space-y-6">
      {/* Pending Payment Banner for existing users */}
      {hasPendingInvoice && (
        <Alert variant="warning">
          <div className="flex items-center justify-between">
            <div>
              <p className="font-medium">Payment Required</p>
              <p className="text-sm">Complete your payment to continue using all features</p>
            </div>
            <Button
              variant="primary"
              size="sm"
              onClick={() => setShowPaymentModal(true)}
            >
              Pay Now
            </Button>
          </div>
        </Alert>
      )}

      {/* Current Plan Card */}
      <Card>
        <div className="flex items-center justify-between">
          <div>
            <h2 className="text-lg font-semibold">{user?.account?.plan?.name}</h2>
            <Badge variant="solid" tone={statusDisplay.tone}>
              {statusDisplay.label}
            </Badge>
          </div>
          <div className="flex gap-2">
            {canManageBilling && (
              <Button variant="outline" onClick={handleManageBilling}>
                Manage Billing
              </Button>
            )}
            {canUpgrade && (
              <Button variant="primary" onClick={() => setShowUpgradeModal(true)}>
                Upgrade
              </Button>
            )}
          </div>
        </div>
      </Card>

      {/* Credits Card */}
      <Card>
        <div className="flex items-center justify-between">
          <div>
            <p className="text-sm text-gray-500">Credit Balance</p>
            <p className="text-2xl font-bold">{user?.account?.credits?.toLocaleString()}</p>
          </div>
          {canBuyCredits && (
            <Button variant="outline" onClick={() => setShowCreditsModal(true)}>
              Buy Credits
            </Button>
          )}
        </div>
      </Card>

      {/* Billing History */}
      <Card>
        <h3 className="font-semibold mb-4">Billing History</h3>
        <InvoiceTable invoices={invoices} />
      </Card>

      {/* Cancel Subscription - only for active paid users */}
      {canCancel && subscription && (
        <div className="text-center">
          <button
            onClick={() => setShowCancelModal(true)}
            className="text-sm text-gray-500 hover:text-error-500"
          >
            Cancel Subscription
          </button>
        </div>
      )}

      {/* Modals */}
      <PayInvoiceModal
        isOpen={showPaymentModal}
        onClose={() => setShowPaymentModal(false)}
        invoice={invoices.find(i => i.status === 'pending')}
        userCountry={user?.account?.billing_country}
        onSuccess={onRefresh}
      />

      <BuyCreditsModal
        isOpen={showCreditsModal}
        onClose={() => setShowCreditsModal(false)}
        userCountry={user?.account?.billing_country}
        onSuccess={onRefresh}
      />
    </div>
  );
}

2.7 Update BankTransferForm to Load Details from Backend

File: frontend/src/components/billing/BankTransferForm.tsx

REMOVE: Hardcoded bank details

ADD: Props for bank details from parent:

interface BankTransferFormProps {
  bankDetails: {
    bank_name: string;
    account_title: string;
    account_number: string;
    iban: string;
    swift_code: string;
  } | null;
  invoiceNumber: string;
  amount: string;
  onSubmit: (data: BankTransferFormData) => void;
  loading: boolean;
}

export default function BankTransferForm({
  bankDetails,
  invoiceNumber,
  amount,
  onSubmit,
  loading,
}: BankTransferFormProps) {
  if (!bankDetails) {
    return <LoadingSpinner />;
  }

  return (
    <div className="space-y-4">
      {/* Bank Details - FROM BACKEND */}
      <div className="p-4 bg-blue-50 rounded-lg">
        <h4 className="font-medium mb-2">Bank Transfer Details</h4>
        <div className="text-sm space-y-1">
          <p><span className="font-medium">Bank:</span> {bankDetails.bank_name}</p>
          <p><span className="font-medium">Account Title:</span> {bankDetails.account_title}</p>
          <p><span className="font-medium">Account #:</span> {bankDetails.account_number}</p>
          <p><span className="font-medium">IBAN:</span> {bankDetails.iban}</p>
          <p><span className="font-medium">Reference:</span> {invoiceNumber}</p>
          <p><span className="font-medium">Amount:</span> {amount}</p>
        </div>
      </div>

      {/* Form fields */}
      {/* ... */}
    </div>
  );
}

2.8 Remove PendingPaymentBanner.tsx

File: frontend/src/components/billing/PendingPaymentBanner.tsx

ACTION: KEPT - Still useful for showing on other pages (dashboard, etc.) The full-page PendingPaymentView replaces this for the /account/plans page when dealing with new users. For existing users with missed payments, the banner can still appear on other pages.


2.9 Clean Up billing.api.ts

File: frontend/src/services/billing.api.ts

REMOVE:

  • detectCountry() function if exists - Not found (never existed)
  • Any unused payment config functions

KEEP:

  • All gateway functions (Stripe, PayPal)
  • Invoice/Payment CRUD
  • subscribeToPlan() and purchaseCredits() helpers

2.10 Clean Up authStore.ts

File: frontend/src/store/authStore.ts

UPDATE register() to NOT expect checkout_url:

register: async (registerData) => {
  const response = await fetch(`${API_BASE_URL}/v1/auth/register/`, {
    method: 'POST',
    body: JSON.stringify({
      email: registerData.email,
      password: registerData.password,
      username: registerData.username,
      first_name: registerData.first_name,
      last_name: registerData.last_name,
      account_name: registerData.account_name,
      plan_slug: registerData.plan_slug,
      billing_country: registerData.billing_country,
    }),
  });

  const data = await response.json();

  if (!response.ok) {
    throw new Error(data.error || 'Registration failed');
  }

  // Set auth state
  set({
    user: data.user,
    token: data.tokens.access,
    refreshToken: data.tokens.refresh,
    isAuthenticated: true,
  });

  return data.user;  // NO checkout_url
}

Phase 3: Remove Dead Code & Fields

3.1 Backend - Remove Unused Fields

File: backend/igny8_core/auth/models.py

Consider removing if truly unused (verify first):

  • Any temporary migration fields

Status: Deferred - no critical dead code identified

3.2 Backend - Clean Up Unused Code in RegisterSerializer

File: backend/igny8_core/auth/urls.py

REMOVE:

  • Stripe checkout session creation code - REMOVED
  • PayPal order creation code - REMOVED
  • All checkout_url generation logic - REMOVED

3.3 Frontend - Remove Files

DELETE these files:

  • frontend/src/pages/AuthPages/SignUpPK.tsx - DELETED
  • Any country detection utility files - None found

Status: SignUpPK.tsx deleted successfully

3.4 Frontend - Remove Unused Routes

File: Routes configuration

REMOVE:

<Route path="/signup/pk" ... />

Done: Route now redirects to main SignUp component


Phase 4: Database Migrations

4.1 Create Migration for WebhookEvent Model

python manage.py makemigrations billing --name add_webhook_event_model
python manage.py migrate

Done: Migration 0029_add_webhook_event_and_manual_reference_constraint applied

4.2 Create Migration for Manual Reference Uniqueness

python manage.py makemigrations billing --name make_manual_reference_unique
python manage.py migrate

Done: Included in migration 0029


Phase 5: Testing Checklist (Manual Testing Required)

5.1 Signup Flow Tests

  • Signup with free plan → redirects to dashboard/sites
  • Signup with paid plan → redirects to /account/plans
  • Signup page shows country dropdown with auto-detection
  • Country dropdown shows all countries from backend
  • No payment method selection on signup page

5.2 PendingPaymentView Tests (New Users)

  • Shows for new user with pending_payment status
  • Shows correct invoice details and plan name
  • Shows Stripe + PayPal for global users
  • Shows Stripe + Bank Transfer for PK users
  • Stripe payment redirects and returns correctly
  • PayPal payment redirects and returns correctly
  • Bank transfer form submits correctly
  • Bank details loaded from backend (not hardcoded)

5.3 ActiveSubscriptionView Tests (Existing Users)

  • Shows for users who have paid before
  • Shows pending payment banner if invoice overdue
  • Upgrade button disabled when payment pending
  • Cancel button only shows for active subscriptions
  • Manage Billing only shows for Stripe users
  • Credit purchase works via modal
  • Invoice payment works via modal

5.4 Webhook Tests

  • Stripe webhook processes only once (idempotency)
  • PayPal webhook validates signature
  • PayPal capture only processes once (idempotency)
  • PayPal amount validation catches mismatches
  • Webhook events stored in WebhookEvent model

5.5 Security Tests

  • Invalid PayPal webhook signature returns 400
  • Duplicate Stripe webhook doesn't create duplicate payment
  • Duplicate PayPal capture returns already_captured response
  • Amount manipulation in PayPal returns error

Implementation Order

Day 1: Backend Security Fixes

  1. Enable PayPal webhook signature verification
  2. Add Stripe webhook idempotency
  3. Add PayPal capture idempotency
  4. Add PayPal amount validation
  5. Fix refund module imports
  6. Add WebhookEvent model
  7. Run migrations

Day 2: Backend Simplification

  1. Simplify RegisterSerializer (remove checkout creation)
  2. Simplify RegisterView (remove checkout_url response)
  3. Add CountryListView endpoint
  4. Add manual_reference uniqueness migration
  5. Test registration flow

Day 3: Frontend Signup Simplification

  1. Simplify SignUpFormUnified.tsx
  2. Remove /signup/pk route
  3. Update authStore register function
  4. Test signup flow for all plans

Day 4: Frontend Plans Page Refactor

  1. Create PendingPaymentView component
  2. Create ActiveSubscriptionView component
  3. Refactor PlansAndBillingPage to use new views
  4. Update BankTransferForm to use backend details
  5. Test new user payment flow

Day 5: Cleanup & Testing

  1. Remove PendingPaymentBanner (or repurpose)
  2. Clean up billing.api.ts
  3. Remove dead code
  4. Full integration testing
  5. Update documentation

Files Changed Summary

Backend Files to Modify

File Changes
views/paypal_views.py Enable signature verification, add idempotency, add amount validation
views/stripe_views.py Add idempotency check
views/refund_views.py Fix imports
models.py Add WebhookEvent model
auth/serializers.py Remove checkout creation
auth/views.py Remove checkout_url, add CountryListView
auth/urls.py Add countries/ endpoint

Backend Migrations

Migration Purpose
add_webhook_event_model Audit trail for webhooks
make_manual_reference_unique Prevent duplicate references

Frontend Files to Modify

File Changes
SignUpFormUnified.tsx Remove payment selection, add country dropdown
PlansAndBillingPage.tsx Refactor to use new view components
authStore.ts Remove checkout_url handling
billing.api.ts Clean up unused functions

Frontend Files to Create

File Purpose
views/PendingPaymentView.tsx Full-page payment for new users
views/ActiveSubscriptionView.tsx Dashboard for existing users
components/BankTransferForm.tsx Reusable bank transfer form

Frontend Files to Delete

File Reason
Route for /signup/pk Consolidated into single signup
PendingPaymentBanner.tsx Replaced by PendingPaymentView (optional - may keep for other pages)

Verification Commands

Backend Verification

# Test Stripe config
curl http://localhost:8000/v1/billing/stripe/config/ -H "Authorization: Bearer $TOKEN"

# Test PayPal config
curl http://localhost:8000/v1/billing/paypal/config/ -H "Authorization: Bearer $TOKEN"

# Test country list
curl http://localhost:8000/v1/auth/countries/

# Test registration (no checkout_url in response)
curl -X POST http://localhost:8000/v1/auth/register/ \
  -H "Content-Type: application/json" \
  -d '{"email":"test@test.com","password":"test123","plan_slug":"starter","billing_country":"US"}'

Frontend Verification

# Build check
npm run build

# Type check
npm run typecheck

# Lint
npm run lint

Success Criteria

  1. Signup flow simplified - No payment redirect from signup
  2. Single payment interface - All payments through /account/plans
  3. Security issues fixed - Webhook verification, idempotency

🚨 CRITICAL ISSUES AFTER REFACTOR

🔴 NEW CRITICAL BUGS INTRODUCED IN RECENT REFACTORS

Issue #1: Payment Gateway Country Filtering Broken

# Severity Issue Location Line
1 🔴 CRITICAL getAvailablePaymentGateways() called without country code - returns PayPal for ALL users including PK PlansAndBillingPage.tsx ~251
2 🔴 CRITICAL getAvailablePaymentGateways() called without country code in PaymentGatewaySelector PaymentGatewaySelector.tsx -
3 🔴 CRITICAL availableGateways initialized with manual: true (Bank Transfer shows for ALL users by default) PlansAndBillingPage.tsx -
4 🔴 CRITICAL PayPal button visible in Buy Credits section for PK users PlansAndBillingPage.tsx -
5 🔴 CRITICAL PayPal button visible in Upgrade Modal for PK users PlansAndBillingPage.tsx -
6 🔴 CRITICAL Bank Transfer button visible in Upgrade Modal for NON-PK users PlansAndBillingPage.tsx -

Root Cause:

// CURRENT (BROKEN) - Line ~251:
const gateways = await getAvailablePaymentGateways();

// SHOULD BE:
const billingCountry = user?.account?.billing_country || 'US';
const gateways = await getAvailablePaymentGateways(billingCountry);

Impact:

  • PK users can see and attempt PayPal payments (NOT ALLOWED)
  • Non-PK users see Bank Transfer option (WRONG)
  • Payment method selection doesn't respect country restrictions

Issue #2: Stripe Return Flow Broken After Refactor

Status: 🔴 BROKEN - Payments not activating accounts

Evidence:

  • User completes payment on Stripe
  • Stripe redirects to /account/plans?success=true
  • Invoice remains status='pending'
  • Account remains status='pending_payment'
  • Notification shows "payment successful" but account not activated

Root Cause:

  1. Frontend redirect URL changed from /plans to /account/plans
  2. No backend endpoint to handle return URL verification
  3. Relies entirely on webhook which may be delayed
  4. Frontend has no way to force-check payment status

Missing Flow:

User Returns → Check URL params → ❌ NO BACKEND CALL
                                 → ❌ No status verification
                                 → Shows old cached status

Should Be:

User Returns → Extract session_id from URL 
            → Call /v1/billing/verify-payment-status/?session_id=xxx
            → Backend checks with Stripe
            → Return updated account/subscription status
            → Refresh UI

Issue #3: PayPal Return Flow Broken After Refactor

Status: 🔴 BROKEN - "Payment not captured" error

Evidence:

  • User approves payment on PayPal
  • PayPal redirects to /account/plans?paypal=success&token=xxx
  • Frontend shows "Payment not captured" error
  • Order remains uncaptured

Root Cause:

  1. Frontend expects to call /v1/billing/paypal/capture-order/ on return
  2. But order_id is not in URL params (PayPal uses token param)
  3. Capture endpoint requires order_id not token
  4. Mismatch between PayPal redirect params and capture endpoint

Current Broken Flow:

PayPal Redirect → ?paypal=success&token=EC-xxx
                → Frontend tries to extract order_id
                → ❌ No order_id in URL
                → ❌ Capture fails

Should Be:

PayPal Redirect → ?paypal=success&token=EC-xxx
                → Frontend stores order_id before redirect
                → On return, retrieve order_id from storage
                → Call capture with correct order_id

Issue #4: Invoice Currency Mismatch

# Severity Issue Location
7 🟡 HIGH Buy Credits always shows $ (USD) even for PK users using Bank Transfer (should show PKR) PlansAndBillingPage.tsx
8 🟡 HIGH Quick Upgrade cards always show $ (USD) for all users PlansAndBillingPage.tsx
9 🟡 HIGH Upgrade Modal shows $ for all plans even when Bank Transfer (PKR) selected PlansAndBillingPage.tsx
10 🟢 MEDIUM Invoice created with currency depends on account.payment_method which may not be set at signup invoice_service.py

Root Cause:

  • Invoice currency determined at creation time based on account.payment_method
  • At signup, account.payment_method may not be set
  • Frontend hardcodes $ symbol
  • No dynamic currency switching based on selected gateway

Issue #5: "Manage Billing" Logic Error

# Severity Issue Location
11 🟡 HIGH "Manage Billing" shows for users with Stripe available, not users who paid via Stripe PlansAndBillingPage.tsx

Root Cause:

// CURRENT (WRONG):
const canManageBilling = availableGateways.stripe && hasActivePlan;

// SHOULD BE:
const canManageBilling = user?.account?.payment_method === 'stripe' 
                      && user?.account?.stripe_customer_id
                      && hasActivePlan;

Impact:

  • PK users who paid via Bank Transfer see "Manage Billing" button
  • Clicking it tries to redirect to Stripe portal (which doesn't exist for them)

Issue #6: Plan Status Display Logic Error

# Audit Issue Claimed Status Actual Status
1 Plan shows "Active" even with unpaid invoice Claimed fixed Still using hasActivePlan based on plan existence only
2 "Manage Billing" shown to non-Stripe users Claimed fixed Shows for anyone with availableGateways.stripe
3 Cancel Subscription available when account pending Claimed fixed Logic not properly implemented
4 Payment gateway selection not synced with account Claimed fixed Gateway determined without country

Root Cause:

// CURRENT (WRONG):
const hasActivePlan = !!user?.account?.plan && user?.account?.plan?.slug !== 'free';

// SHOULD CHECK:
const hasActivePlan = user?.account?.status === 'active' 
                   && user?.account?.plan?.slug !== 'free'
                   && !hasPendingInvoice;

Issue #7: Backend Payment Method Endpoint Returns Wrong Data

# Severity Issue Location
12 🟢 MEDIUM Backend list_payment_methods endpoint returns ALL enabled methods ignoring country_code billing_views.py

Evidence:

# Current endpoint ignores country filtering
def list_payment_methods(request):
    methods = PaymentMethodConfig.objects.filter(is_enabled=True)
    # ❌ No country-based filtering
    return Response(methods)

Should Be:

def list_payment_methods(request):
    country = request.query_params.get('country_code', 'US')
    methods = PaymentMethodConfig.objects.filter(
        is_enabled=True,
        country_code=country
    )
    return Response(methods)

🔴 CRITICAL: Payment Return Flows NOT WORKING

Stripe Return Flow - BROKEN

Current State:

User pays on Stripe
    ↓
Stripe redirects to: /account/plans?success=true&session_id=cs_xxx
    ↓
Frontend PlansAndBillingPage loads
    ↓
❌ NO API CALL to verify payment
    ↓
Shows cached user data (still pending_payment)
    ↓
User sees "Complete your payment" (even though they just paid)
    ↓
Webhook eventually fires (5-30 seconds later)
    ↓
Page doesn't auto-refresh
    ↓
User must manually refresh to see activation

What's Missing:

  1. No /v1/billing/verify-stripe-return/ endpoint
  2. Frontend doesn't extract session_id from URL
  3. No status polling/refresh mechanism
  4. No loading state while verification happens

PayPal Return Flow - BROKEN

Current State:

User approves on PayPal
    ↓
PayPal redirects to: /account/plans?paypal=success&token=EC-xxx&PayerID=yyy
    ↓
Frontend tries to capture payment
    ↓
❌ Can't find order_id (only has token)
    ↓
Capture API call fails
    ↓
Shows error: "Payment not captured"
    ↓
User stuck with pending payment

What's Missing:

  1. order_id not persisted before redirect
  2. No mapping from tokenorder_id
  3. Capture endpoint can't proceed without order_id

Bank Transfer Flow - Working

Current State: (This one works correctly)

User submits bank transfer proof
    ↓
POST /v1/billing/payments/manual/
    ↓
Creates Payment record (status=pending)
    ↓
Admin approves payment
    ↓
Account activated
    ↓
✅ Works as expected

📊 COMPLETE END-TO-END PAYMENT WORKFLOW DIAGRAMS

🔵 STRIPE FLOW - US/GLOBAL USERS (Subscription)

┌────────────────────────────────────────────────────────────────────┐
│                    PHASE 1: SIGNUP                                  │
└────────────────────────────────────────────────────────────────────┘

User visits /signup
    ↓
Fills form: email, password, name, country=US, plan=Starter ($29/mo)
    ↓
Clicks "Create Account"
    ↓
POST /v1/auth/register/
    └─> Body: {
            email, password, username,
            billing_country: 'US',
            plan_slug: 'starter'
        }

BACKEND ACTIONS:
    1. Create User
    2. Create Account (status='pending_payment', plan=Starter, credits=0)
    3. Create Subscription (status='pending_payment')
    4. Create Invoice (status='pending', currency='USD', amount=29.00)
    5. Return: { user, tokens, ... }  // ❌ NO checkout_url

Frontend redirects to → /account/plans


┌────────────────────────────────────────────────────────────────────┐
│                    PHASE 2: PAYMENT PAGE                            │
└────────────────────────────────────────────────────────────────────┘

/account/plans loads
    ↓
Checks: accountStatus='pending_payment' && hasEverPaid=false
    ↓
Shows PendingPaymentView
    ↓
Displays Invoice #INV-260100001, Amount: $29.00
    ↓
Payment methods: [Stripe ✅, PayPal ✅]  (country=US)
    ↓
User selects Stripe
    ↓
Clicks "Pay $29.00"
    ↓

POST /v1/billing/stripe/subscribe/
    └─> Body: {
            plan_slug: 'starter',
            payment_method: 'stripe',
            return_url: 'http://localhost:5173/account/plans?success=true',
            cancel_url: 'http://localhost:5173/account/plans?canceled=true'
        }

BACKEND ACTIONS (stripe_views.py - StripeSubscribeView):
    1. Get plan, account
    2. Create Stripe Checkout Session
       - mode: 'subscription'
       - line_items: [{ price: plan.stripe_price_id, quantity: 1 }]
       - customer_email: user.email
       - metadata: { account_id, plan_id, type: 'subscription' }
       - success_url: return_url
       - cancel_url: cancel_url
    3. Return { redirect_url: session.url }

Frontend redirects to → session.url (Stripe hosted page)


┌────────────────────────────────────────────────────────────────────┐
│                    PHASE 3: STRIPE PAYMENT                          │
└────────────────────────────────────────────────────────────────────┘

User on Stripe page
    ↓
Enters card details
    ↓
Clicks "Pay"
    ↓
Stripe processes payment
    ↓
Payment succeeds
    ↓
Stripe redirects to → /account/plans?success=true&session_id=cs_xxx


┌────────────────────────────────────────────────────────────────────┐
│                    PHASE 4: RETURN TO APP (❌ BROKEN)              │
└────────────────────────────────────────────────────────────────────┘

Browser lands on /account/plans?success=true&session_id=cs_xxx
    ↓
PlansAndBillingPage.tsx loads
    ↓
useEffect runs → loadBillingData()
    ↓
❌ BUG: No code to check URL params for session_id
❌ BUG: No API call to verify payment status
❌ BUG: Just loads user data from cache/API (still shows pending)
    ↓
User sees: "Complete Your Subscription" (same pending view)
    ↓
User confused: "I just paid!"


┌────────────────────────────────────────────────────────────────────┐
│                    PHASE 5: WEBHOOK (5-30 SEC LATER)               │
└────────────────────────────────────────────────────────────────────┘

Stripe sends webhook → POST /v1/billing/stripe/webhook/
    └─> Event: checkout.session.completed

BACKEND ACTIONS (stripe_views.py - _handle_checkout_completed):
    ✅ 1. Idempotency check - session_id in Payment.metadata
    ✅ 2. Get account from metadata
    ✅ 3. Retrieve Stripe subscription details
    ✅ 4. Create/Update Subscription record:
          - status = 'active'
          - stripe_subscription_id = 'sub_xxx'
          - current_period_start, current_period_end
    ✅ 5. Get or create Invoice (prevents duplicates)
    ✅ 6. Mark Invoice as PAID:
          - status = 'paid'
          - paid_at = now
    ✅ 7. Create Payment record:
          - status = 'succeeded'
          - approved_at = now
          - stripe_payment_intent_id
          - metadata: { checkout_session_id, subscription_id }
    ✅ 8. Create/Update AccountPaymentMethod:
          - type = 'stripe'
          - is_verified = True
          - is_default = True
    ✅ 9. Add credits:
          - CreditService.add_credits(account, plan.included_credits)
    ✅ 10. Update Account:
          - status = 'active'
          - plan = plan
    ❌ 11. NO email sent
    ❌ 12. NO real-time notification to frontend

Returns 200 to Stripe


┌────────────────────────────────────────────────────────────────────┐
│                    PHASE 6: USER EXPERIENCE                         │
└────────────────────────────────────────────────────────────────────┘

User still on /account/plans
    ↓
Page shows pending payment (cached data)
    ↓
❌ Page doesn't auto-refresh
❌ No websocket notification
    ↓
User must MANUALLY refresh page
    ↓
On refresh: loadBillingData() fetches new status
    ↓
NOW shows: Active plan, credits added
    ↓
✅ Finally works after manual refresh


┌────────────────────────────────────────────────────────────────────┐
│                    CURRENT BUGS SUMMARY                             │
└────────────────────────────────────────────────────────────────────┘

🔴 CRITICAL BUGS:
1. No payment verification on return URL
2. User sees pending state after successful payment
3. Requires manual refresh to see activation
4. No loading/polling state
5. Poor UX - user thinks payment failed

✅ WORKING CORRECTLY:
1. Webhook processing
2. Account activation
3. Credit addition
4. Subscription creation
5. Invoice marking as paid

🟡 PAYPAL FLOW - US/GLOBAL USERS (Subscription)

┌────────────────────────────────────────────────────────────────────┐
│                    PHASE 1-2: SAME AS STRIPE                        │
└────────────────────────────────────────────────────────────────────┘

(User signup, account creation, redirect to /account/plans)


┌────────────────────────────────────────────────────────────────────┐
│                    PHASE 3: PAYPAL ORDER CREATION                   │
└────────────────────────────────────────────────────────────────────┘

User on /account/plans (PendingPaymentView)
    ↓
Selects PayPal
    ↓
Clicks "Pay with PayPal"
    ↓

POST /v1/billing/paypal/subscribe/
    └─> Body: {
            plan_slug: 'starter',
            payment_method: 'paypal',
            return_url: 'http://localhost:5173/account/plans?paypal=success',
            cancel_url: 'http://localhost:5173/account/plans?paypal=cancel'
        }

BACKEND ACTIONS (paypal_views.py - PayPalSubscribeView):
    1. Get plan, account
    2. Create PayPal Order:
       - intent: 'CAPTURE'
       - purchase_units: [{
           amount: { currency_code: 'USD', value: '29.00' },
           custom_id: account.id,
           description: plan.name
         }]
       - application_context: {
           return_url: return_url,
           cancel_url: cancel_url
         }
    3. Extract order_id from response
    4. Extract approve link from response.links
    5. ❌ BUG: order_id NOT stored anywhere for later retrieval
    6. Return { redirect_url: approve_link }

Frontend:
    ❌ BUG: Doesn't store order_id before redirect
    Redirects to → approve_link (PayPal page)


┌────────────────────────────────────────────────────────────────────┐
│                    PHASE 4: PAYPAL APPROVAL                         │
└────────────────────────────────────────────────────────────────────┘

User on PayPal page
    ↓
Logs in to PayPal
    ↓
Clicks "Approve"
    ↓
PayPal redirects to → /account/plans?paypal=success&token=EC-xxx&PayerID=yyy
    ↓
❌ BUG: URL has 'token', not 'order_id'


┌────────────────────────────────────────────────────────────────────┐
│                    PHASE 5: CAPTURE ATTEMPT (❌ BROKEN)            │
└────────────────────────────────────────────────────────────────────┘

Browser lands on /account/plans?paypal=success&token=EC-xxx
    ↓
PlansAndBillingPage.tsx detects ?paypal=success
    ↓
Tries to extract order_id from URL params
    ↓
❌ BUG: URL has 'token=EC-xxx', not 'order_id'
❌ BUG: No code to convert token → order_id
❌ BUG: No stored order_id from Phase 3
    ↓
Calls POST /v1/billing/paypal/capture-order/
    └─> Body: { order_id: undefined }  // ❌ MISSING
    ↓
Backend returns 400: "order_id required"
    ↓
Frontend shows: "Payment not captured"
    ↓
User stuck with pending payment


┌────────────────────────────────────────────────────────────────────┐
│                    WHAT SHOULD HAPPEN (IF WORKING)                  │
└────────────────────────────────────────────────────────────────────┘

Frontend should:
    1. Store order_id in localStorage before redirect
    2. On return, retrieve order_id from localStorage
    3. Call capture with correct order_id

OR:

Backend should:
    1. Accept token parameter
    2. Look up order_id from token via PayPal API
    3. Then proceed with capture

CAPTURE FLOW (paypal_views.py - PayPalCaptureOrderView):
    ✅ 1. Idempotency check - order_id already captured
    ✅ 2. Call PayPal API to capture order
    ✅ 3. Extract payment type from metadata
    ✅ 4. If subscription:
          - _process_subscription_payment()
          - Create Subscription (status='active')
          - Create Invoice
          - Mark Invoice paid
          - Create Payment record
          - Create AccountPaymentMethod (type='paypal')
          - Add credits
          - Activate account
    ✅ 5. Return success

But currently this never executes due to missing order_id.


┌────────────────────────────────────────────────────────────────────┐
│                    CURRENT BUGS SUMMARY                             │
└────────────────────────────────────────────────────────────────────┘

🔴 CRITICAL BUGS:
1. order_id not persisted before redirect
2. Return URL has 'token', not 'order_id'
3. No token → order_id mapping
4. Capture fails completely
5. Payment left uncaptured on PayPal
6. User sees error and can't complete signup

❌ COMPLETELY BROKEN - PayPal payments don't work

🟢 BANK TRANSFER FLOW - PAKISTAN USERS

┌────────────────────────────────────────────────────────────────────┐
│                    PHASE 1-2: SAME AS STRIPE                        │
└────────────────────────────────────────────────────────────────────┘

User signup with country=PK, plan=Starter
    ↓
Account created (status='pending_payment')
    ↓
Invoice created (currency='PKR', amount=8,499.00)
    ↓
Redirect to /account/plans


┌────────────────────────────────────────────────────────────────────┐
│                    PHASE 3: BANK TRANSFER FORM                      │
└────────────────────────────────────────────────────────────────────┘

/account/plans loads
    ↓
Shows PendingPaymentView
    ↓
Payment methods: [Stripe ✅, Bank Transfer ✅]  (country=PK)
    ↓
❌ BUG: May also show PayPal if country not passed correctly
    ↓
User selects Bank Transfer
    ↓

Loads bank details via GET /v1/billing/payment-configs/payment-methods/?country_code=PK&payment_method=bank_transfer
    ↓
Backend returns:
    {
        bank_name: "HBL",
        account_title: "IGNY8 Platform",
        account_number: "12345678",
        iban: "PK36HABB0012345678901234",
        swift_code: "HABBPKKA",
        instructions: "..."
    }
    ↓
Displays bank details to user
    ↓
User transfers PKR 8,499 to bank account
    ↓
User fills form:
    - Transaction Reference: TRX123456
    - Upload proof (receipt image)
    - Notes: "Paid from account ending 9876"
    ↓
Clicks "Submit Payment Proof"
    ↓

POST /v1/billing/payments/manual/
    └─> Body: {
            invoice_id: <invoice_id>,
            payment_method: 'bank_transfer',
            amount: 8499.00,
            manual_reference: 'TRX123456',
            manual_notes: '...',
            proof_url: 'https://...'
        }

BACKEND ACTIONS (billing_views.py):
    ✅ 1. Get invoice
    ✅ 2. Create Payment record:
          - status = 'pending'  (awaiting admin approval)
          - payment_method = 'bank_transfer'
          - manual_reference = 'TRX123456'
          - manual_proof_url = proof
          - approved_at = None
          - approved_by = None
    ✅ 3. Return success
    ❌ 4. NO email to admin
    ❌ 5. NO email to user

Frontend shows: "Payment submitted for approval"


┌────────────────────────────────────────────────────────────────────┐
│                    PHASE 4: ADMIN APPROVAL                          │
└────────────────────────────────────────────────────────────────────┘

Admin logs into Django Admin
    ↓
Navigates to Payments
    ↓
Sees Payment (status='pending', reference='TRX123456')
    ↓
Views proof image
    ↓
Verifies transaction in bank statement
    ↓
Changes status to 'succeeded'
    ↓
Sets approved_by = admin_user
    ↓
Clicks "Save"
    ↓

BACKEND ACTIONS (admin.py - save_model or signal):
    ✅ 1. Mark Invoice as paid
    ✅ 2. Update Subscription status = 'active'
    ✅ 3. Add credits to account
    ✅ 4. Update Account:
          - status = 'active'
          - payment_method = 'bank_transfer'
    ✅ 5. Create AccountPaymentMethod:
          - type = 'bank_transfer'
          - is_verified = True
          - is_default = True
    ❌ 6. NO email to user about approval


┌────────────────────────────────────────────────────────────────────┐
│                    PHASE 5: USER SEES ACTIVATION                    │
└────────────────────────────────────────────────────────────────────┘

User refreshes /account/plans
    ↓
loadBillingData() fetches updated status
    ↓
Shows: Active plan, credits available
    ↓
✅ Works correctly after admin approval


┌────────────────────────────────────────────────────────────────────┐
│                    CURRENT BUGS SUMMARY                             │
└────────────────────────────────────────────────────────────────────┘

🟡 MINOR BUGS:
1. No email notification to admin
2. No email to user on approval
3. Currency symbol shows '$' in UI (should show 'Rs')
4. ❌ PayPal may show for PK users if country filtering broken

✅ WORKING CORRECTLY:
1. Bank details from backend
2. Manual payment submission
3. Admin approval flow
4. Account activation after approval

💳 CREDIT PURCHASE FLOWS

Stripe Credit Purchase (US/Global)

User on /account/plans (Active account)
    ↓
Clicks "Buy Credits"
    ↓
Selects package: 1000 credits for $50
    ↓
Clicks "Buy with Stripe"
    ↓

POST /v1/billing/stripe/purchase-credits/
    └─> Body: {
            credit_package_id: <package_id>,
            payment_method: 'stripe',
            return_url: '...',
            cancel_url: '...'
        }

BACKEND:
    1. Create Stripe Checkout Session (mode='payment')
    2. metadata: { credit_package_id, credit_amount, type: 'credits' }
    3. Return redirect_url

User pays on Stripe → redirects back
    ↓
❌ SAME BUG: No payment verification on return
    ↓
Webhook fires → _add_purchased_credits()
    ↓
✅ Creates Invoice
✅ Marks paid
✅ Creates Payment
✅ Adds credits
    ↓
User must manual refresh to see new credits

PayPal Credit Purchase (US/Global)

User clicks "Buy with PayPal"
    ↓
POST /v1/billing/paypal/purchase-credits/
    ↓
Creates PayPal Order
    ↓
❌ SAME BUGS: order_id not stored, token not mapped
    ↓
User approves on PayPal
    ↓
Return URL has token
    ↓
❌ Capture fails - no order_id
    ↓
Payment not completed

Bank Transfer Credit Purchase (PK)

User selects amount → bank transfer form
    ↓
Submits proof → creates Payment (pending)
    ↓
Admin approves → adds credits
    ↓
✅ Works correctly

🔄 RECURRING PAYMENT FLOWS

Stripe Subscription Renewal

30 days after activation
    ↓
Stripe auto-charges customer
    ↓
Stripe sends webhook: invoice.paid
    ↓

BACKEND (_handle_invoice_paid):
    ✅ 1. Skip if billing_reason = 'subscription_create'
    ✅ 2. Get Subscription from stripe_subscription_id
    ✅ 3. Add renewal credits:
          CreditService.add_credits(account, plan.included_credits)
    ✅ 4. Update subscription period dates
    ❌ 5. NO email sent

✅ WORKS CORRECTLY

Stripe Payment Failure

Renewal charge fails (card declined)
    ↓
Stripe sends webhook: invoice.payment_failed
    ↓

BACKEND (_handle_invoice_payment_failed):
    ✅ 1. Update Subscription status = 'past_due'
    ✅ 2. Log failure
    ❌ 3. TODO: Send email (not implemented)

⚠️ PARTIAL - Updates status but no notification

PayPal Recurring (If using PayPal Subscriptions)

PayPal sends: BILLING.SUBSCRIPTION.ACTIVATED
    ↓
BACKEND (_handle_subscription_activated):
    ✅ Creates/updates Subscription
    ✅ Adds credits
    ✅ Activates account

PayPal sends: BILLING.SUBSCRIPTION.PAYMENT.FAILED
    ↓
BACKEND (_handle_subscription_payment_failed):
    ✅ Updates status = 'past_due'
    ❌ TODO: Send email

⚠️ PARTIAL - Works but no notifications

🎯 ROOT CAUSES OF ALL ISSUES

1. Country Filtering Not Applied

One line fix in PlansAndBillingPage.tsx:

// Line ~251 - ADD country parameter:
const billingCountry = user?.account?.billing_country || 'US';
const gateways = await getAvailablePaymentGateways(billingCountry);

2. No Payment Return Verification

Need new backend endpoints:

# stripe_views.py
class VerifyStripeReturnView(APIView):
    def get(self, request):
        session_id = request.query_params.get('session_id')
        # Retrieve session from Stripe
        # Check payment status
        # Return account/subscription status
        
# paypal_views.py  
class VerifyPayPalReturnView(APIView):
    def get(self, request):
        token = request.query_params.get('token')
        # Map token to order_id
        # Check order status with PayPal
        # Return account status

Frontend needs to call these on return:

useEffect(() => {
    const params = new URLSearchParams(location.search);
    
    if (params.get('success') && params.get('session_id')) {
        verifyStripePayment(params.get('session_id'));
    }
    
    if (params.get('paypal') === 'success' && params.get('token')) {
        verifyPayPalPayment(params.get('token'));
    }
}, [location.search]);

3. PayPal Order ID Not Persisted

Fix in frontend before redirect:

// Before redirecting to PayPal:
const { order_id, redirect_url } = await createPayPalOrder();
localStorage.setItem('paypal_order_id', order_id);
window.location.href = redirect_url;

// On return:
const order_id = localStorage.getItem('paypal_order_id');
await capturePayPalOrder(order_id);
localStorage.removeItem('paypal_order_id');

4. Currency Display Hardcoded

Fix dynamic currency:

const getCurrencySymbol = (country: string, method: string) => {
    if (method === 'bank_transfer' && country === 'PK') return 'Rs';
    return '$';
};

const getCurrencyCode = (country: string, method: string) => {
    if (method === 'bank_transfer' && country === 'PK') return 'PKR';
    return 'USD';
};

5. Status Logic Inconsistent

Fix hasActivePlan:

const hasActivePlan = user?.account?.status === 'active' 
                   && user?.account?.plan?.slug !== 'free'
                   && !hasPendingInvoice;

const canManageBilling = user?.account?.payment_method === 'stripe'
                      && user?.account?.stripe_customer_id
                      && hasActivePlan;

📋 COMPLETE ISSUE CHECKLIST

# Issue Severity Status Fix Required
1 Country not passed to payment gateway selector 🔴 CRITICAL Open Add country param
2 Stripe return doesn't verify payment 🔴 CRITICAL Open Add verification endpoint
3 PayPal order_id not persisted 🔴 CRITICAL Open Store in localStorage
4 PayPal token not mapped to order_id 🔴 CRITICAL Open Backend mapping or localStorage
5 Invoice remains pending after Stripe success 🔴 CRITICAL Open Add return verification
6 PayPal capture fails on return 🔴 CRITICAL Open Fix order_id persistence
7 Currency hardcoded to USD 🟡 HIGH Open Dynamic currency by country/method
8 Manage Billing shows for wrong users 🟡 HIGH Open Check actual payment_method
9 hasActivePlan logic incorrect 🟡 HIGH Open Check account status properly
10 No real-time status update after payment 🟡 HIGH Open Polling or websocket
11 Backend payment methods ignore country 🟢 MEDIUM Open Filter by country_code
12 No email notifications 🟢 MEDIUM Deferred Add email service
13 No webhook audit trail 🟢 MEDIUM Open Store in WebhookEvent model

🚀 PRIORITY FIX ORDER

IMMEDIATE (Do First):

  1. Fix country filtering in gateway selection
  2. Add Stripe return verification endpoint
  3. Fix PayPal order_id persistence
  4. Test end-to-end payment flows

HIGH PRIORITY (Do Next):

  1. Dynamic currency display
  2. Fix hasActivePlan logic
  3. Fix Manage Billing button logic
  4. Add webhook event storage

MEDIUM PRIORITY (Do After):

  1. Real-time status updates (polling/websocket)
  2. Email notifications
  3. Admin notification for manual payments

End of Critical Issues Analysis 4. Country handling simplified - Dropdown instead of detection API 5. Code cleaned up - No dead routes, no hardcoded values 6. All payment methods work - Stripe, PayPal, Bank Transfer 7. State properly reflected - Badges, buttons based on actual status


Plan created based on comprehensive audit of IGNY8 payment system.