diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4be0ed17..43360ca2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,7 @@
# IGNY8 Change Log
-**Current Version:** 1.4.0
-**Last Updated:** January 6, 2026
+**Current Version:** 1.6.0
+**Last Updated:** January 8, 2026
---
@@ -9,6 +9,7 @@
| Version | Date | Summary |
|---------|------|---------|
+| 1.6.0 | Jan 8, 2026 | **Major** - Payment System Refactor Complete: Stripe, PayPal, Bank Transfer flows finalized; Simplified signup (no payment redirect); Country-based payment rules; Webhook security; PDF invoices |
| 1.5.0 | *Planned* | Image Generation System Overhaul, Quality Tier Refinements, Credit Service Enhancements |
| 1.4.0 | Jan 5, 2026 | **Major** - AI Model Architecture Overhaul, IntegrationProvider Model, AIModelConfig with Credit System, SystemAISettings, Django Admin Reorganization |
| 1.3.2 | Jan 3, 2026 | **Major** - Publishing Scheduler, Onboarding Wizard, Content Calendar, Design System Consolidation, Site Dashboard redesign |
@@ -36,6 +37,173 @@
---
+## v1.6.0 - January 8, 2026
+
+### Major Release: Payment System Refactor Complete
+
+This release completes the comprehensive refactoring of the payment system, including all three payment gateways (Stripe, PayPal, Bank Transfer), simplified signup flow, and enhanced security.
+
+---
+
+### π³ Payment Gateway Integration
+
+**Stripe Integration:**
+- Subscription checkout sessions
+- Credit package checkout
+- Billing portal for subscription management
+- Webhook processing with idempotency
+- Signature verification enforced
+
+**PayPal Integration:**
+- One-time orders for credit packages
+- Subscription orders for plans
+- Order capture flow
+- Webhook signature verification enabled
+- Amount validation on capture
+
+**Bank Transfer (Pakistan):**
+- Manual payment submission
+- File upload for payment proof
+- Admin approval workflow
+- Email notifications on approval/rejection
+
+---
+
+### π Simplified Signup Flow
+
+**Before:** Payment gateway redirect from signup page
+**After:** Signup creates account only, payment on /account/plans
+
+**Changes:**
+- Removed Stripe/PayPal checkout creation from registration
+- Account created with `status='pending_payment'` for paid plans
+- User redirected to Plans & Billing page to complete payment
+- Single `/signup` route (removed `/signup/pk` variant)
+
+---
+
+### π Country-Based Payment Rules
+
+**Global Users (non-PK):**
+- Stripe (Credit/Debit Card) β
+- PayPal β
+- Bank Transfer β
+
+**Pakistan Users (PK):**
+- Stripe (Credit/Debit Card) β
+- PayPal β (not available in Pakistan)
+- Bank Transfer β
+
+---
+
+### π¨ Frontend Components
+
+**New Components:**
+- `PendingPaymentView` - Full-page payment interface for new users
+- `BankTransferForm` - Bank transfer submission with proof upload
+- `PaymentGatewaySelector` - Gateway selection UI
+
+**Updated Components:**
+- `PlansAndBillingPage` - Conditional rendering based on user state
+- `PendingPaymentBanner` - Alert for pending payments
+- `PayInvoiceModal` - Invoice payment modal
+
+---
+
+### π Security Enhancements
+
+- **Webhook Idempotency:** `WebhookEvent` model tracks processed events
+- **PayPal Signature Verification:** Enabled and enforced
+- **Stripe Signature Verification:** Already enforced
+- **Amount Validation:** PayPal capture validates amount matches expected
+- **Manual Reference Uniqueness:** Database constraint prevents duplicates
+
+---
+
+### π PDF Invoice Generation
+
+- Added `reportlab` for professional PDF generation
+- Logo integration from frontend assets
+- Proper formatting (no HTML tags in output)
+- Clean layout with company info, line items, totals
+- Payment information section for paid invoices
+
+---
+
+### ποΈ Database Changes
+
+**New Migration:** `0029_add_webhook_event_and_manual_reference_constraint`
+
+**New Model:** `WebhookEvent`
+```python
+class WebhookEvent(models.Model):
+ provider = models.CharField(choices=['stripe', 'paypal'])
+ event_id = models.CharField(max_length=255)
+ event_type = models.CharField(max_length=100)
+ payload = models.JSONField()
+ status = models.CharField() # pending, processed, failed
+ processed_at = models.DateTimeField(null=True)
+```
+
+**Updated Model:** `Payment`
+- Added unique constraint on `manual_reference`
+
+---
+
+### π Documentation
+
+**Consolidated:** Three payment documentation files merged into one:
+- `PAYMENT-SYSTEM-ARCHITECTURE.md` β Removed
+- `PAYMENT-SYSTEM-AUDIT-REPORT.md` β Removed
+- `PAYMENT-SYSTEM-REFACTOR-PLAN.md` β Removed
+
+**New:** `90-REFERENCE/PAYMENT-SYSTEM.md`
+- Complete payment system documentation
+- All flows, endpoints, models in one file
+- Current state only (no planning artifacts)
+
+---
+
+### π¦ Backend Files Changed
+
+| File | Changes |
+|------|---------|
+| `billing/views/stripe_views.py` | Idempotency, enhanced logging |
+| `billing/views/paypal_views.py` | Signature verification, amount validation, idempotency |
+| `billing/views/refund_views.py` | Fixed imports |
+| `billing/services/stripe_service.py` | Full service implementation |
+| `billing/services/paypal_service.py` | Full service implementation |
+| `billing/services/pdf_service.py` | Professional PDF generation with logo |
+| `billing/services/email_service.py` | Payment notification emails |
+| `billing/models.py` | WebhookEvent model, payment method choices |
+| `auth/serializers.py` | Removed checkout creation from registration |
+| `auth/urls.py` | Added country list endpoint |
+
+---
+
+### π± Frontend Files Changed
+
+| File | Changes |
+|------|---------|
+| `SignUpFormUnified.tsx` | Simplified, removed payment selection |
+| `PlansAndBillingPage.tsx` | Conditional views, state handling |
+| `PendingPaymentView.tsx` | New component for new user payments |
+| `BankTransferForm.tsx` | New component for bank transfers |
+| `PaymentGatewaySelector.tsx` | New component for gateway selection |
+| `billing.api.ts` | Country-based gateway availability |
+| `authStore.ts` | Removed checkout URL handling |
+
+---
+
+### ποΈ Removed
+
+- `/signup/pk` route (consolidated into `/signup`)
+- `SignUpPK.tsx` page component
+- Payment gateway redirect from signup
+- Three separate payment documentation files
+
+---
+
## v1.5.0 - Planned
### Upcoming Release: Image Generation System Overhaul
diff --git a/docs/10-MODULES/BILLING.md b/docs/10-MODULES/BILLING.md
index 4a40b049..d2323945 100644
--- a/docs/10-MODULES/BILLING.md
+++ b/docs/10-MODULES/BILLING.md
@@ -1,10 +1,12 @@
# Billing Module
-**Last Verified:** January 6, 2026
-**Status:** β
Active (Simplified January 2026)
+**Last Verified:** January 8, 2026
+**Status:** β
Active (Payment System Refactored January 2026)
**Backend Path:** `backend/igny8_core/modules/billing/` + `backend/igny8_core/business/billing/`
**Frontend Path:** `frontend/src/pages/Billing/` + `frontend/src/pages/Account/`
+> **Payment System Reference:** For comprehensive payment gateway documentation (Stripe, PayPal, Bank Transfer), see [PAYMENT-SYSTEM.md](../90-REFERENCE/PAYMENT-SYSTEM.md)
+
---
## Quick Reference
diff --git a/docs/90-REFERENCE/PAYMENT-SYSTEM-ARCHITECTURE.md b/docs/90-REFERENCE/PAYMENT-SYSTEM-ARCHITECTURE.md
deleted file mode 100644
index b1a755d8..00000000
--- a/docs/90-REFERENCE/PAYMENT-SYSTEM-ARCHITECTURE.md
+++ /dev/null
@@ -1,1120 +0,0 @@
-# Complete Payment System Architecture
-
-> **Version:** 1.0
-> **Last Updated:** January 7, 2026
-> **Status:** Complete Documentation
-
-This document provides comprehensive documentation of the IGNY8 payment system architecture, including all entry points, services, models, and flows.
-
----
-
-## Table of Contents
-
-1. [System Overview](#system-overview)
-2. [Payment Entry Points (Frontend)](#payment-entry-points-frontend)
-3. [Frontend Payment Services](#frontend-payment-services)
-4. [Backend Endpoints](#backend-endpoints)
-5. [Backend Services](#backend-services)
-6. [Models](#models)
-7. [Payment Method Configuration](#payment-method-configuration)
-8. [Payment Flows](#payment-flows)
-9. [Webhooks](#webhooks)
-
----
-
-## System Overview
-
-### Supported Payment Methods
-
-| Method | Type | Regions | Use Cases |
-|--------|------|---------|-----------|
-| **Stripe** | Credit/Debit Card | Global | Subscriptions, Credit packages |
-| **PayPal** | PayPal account | Global (except PK) | Subscriptions, Credit packages |
-| **Bank Transfer** | Manual | Pakistan (PK) | Subscriptions, Credit packages |
-| **Local Wallet** | Manual | Configurable | Credit packages |
-
-### Payment Method Selection Logic
-
-- **Global Users**: Stripe (Card) + PayPal
-- **Pakistan Users**: Stripe (Card) + Bank Transfer (NO PayPal)
-
----
-
-## Payment Entry Points (Frontend)
-
-### 1. SignUpFormUnified.tsx
-
-**File:** [frontend/src/components/auth/SignUpFormUnified.tsx](../../../frontend/src/components/auth/SignUpFormUnified.tsx)
-
-**Purpose:** Handles payment method selection and checkout redirect during user registration.
-
-#### Key Features (Lines 1-770)
-
-```
-- Payment Method Loading: Lines 105-195
-- Form Submission with Payment: Lines 213-344
-- Payment Gateway Redirect Logic: Lines 315-340
-```
-
-#### Payment Flow:
-
-1. **Loads available payment methods** from `/v1/billing/payment-configs/payment-methods/` (Lines 110-195)
-2. **Determines available options** based on country:
- - Pakistan (`/signup/pk`): Stripe + Bank Transfer
- - Global (`/signup`): Stripe + PayPal
-3. **On form submit** (Lines 213-344):
- - Calls `register()` from authStore
- - Backend returns `checkout_url` for Stripe/PayPal
- - Redirects to payment gateway OR plans page (bank transfer)
-
-#### Data Passed to Backend:
-
-```typescript
-{
- email, password, username, first_name, last_name,
- account_name,
- plan_slug: selectedPlan.slug,
- payment_method: selectedPaymentMethod, // 'stripe' | 'paypal' | 'bank_transfer'
- billing_email,
- billing_country
-}
-```
-
----
-
-### 2. PlansAndBillingPage.tsx
-
-**File:** [frontend/src/pages/account/PlansAndBillingPage.tsx](../../../frontend/src/pages/account/PlansAndBillingPage.tsx)
-
-**Purpose:** Comprehensive billing dashboard for subscription management, credit purchases, and payment history.
-
-#### Key Features (Lines 1-1197)
-
-```
-- Payment Gateway Return Handler: Lines 100-155
-- Plan Selection (handleSelectPlan): Lines 283-310
-- Credit Purchase (handlePurchaseCredits): Lines 330-352
-- Billing Portal (handleManageSubscription): Lines 354-361
-- Invoice Download: Lines 363-376
-```
-
-#### Payment Flows:
-
-**1. Plan Upgrade (handleSelectPlan - Lines 283-310):**
-```typescript
-// For Stripe/PayPal:
-const { redirect_url } = await subscribeToPlan(planId.toString(), selectedGateway);
-window.location.href = redirect_url;
-
-// For Manual/Bank Transfer:
-await createSubscription({ plan_id: planId, payment_method: selectedPaymentMethod });
-```
-
-**2. Credit Purchase (handlePurchaseCredits - Lines 330-352):**
-```typescript
-// For Stripe/PayPal:
-const { redirect_url } = await purchaseCredits(packageId.toString(), selectedGateway);
-window.location.href = redirect_url;
-
-// For Manual/Bank Transfer:
-await purchaseCreditPackage({ package_id: packageId, payment_method: selectedPaymentMethod });
-```
-
-**3. Return URL Handling (Lines 100-155):**
-```typescript
-// PayPal Success - capture order
-if (paypalStatus === 'success' && paypalToken) {
- await capturePayPalOrder(paypalToken, { plan_id, package_id });
-}
-// Stripe Success
-if (success === 'true') {
- toast.success('Subscription activated!');
- await refreshUser();
-}
-```
-
----
-
-### 3. PayInvoiceModal.tsx
-
-**File:** [frontend/src/components/billing/PayInvoiceModal.tsx](../../../frontend/src/components/billing/PayInvoiceModal.tsx)
-
-**Purpose:** Modal for paying pending invoices with multiple payment methods.
-
-#### Key Features (Lines 1-544)
-
-```
-- Payment Method Selection: Lines 55-95
-- Stripe Payment Handler: Lines 115-140
-- PayPal Payment Handler: Lines 142-167
-- Bank Transfer Form: Lines 203-250
-```
-
-#### Payment Method Logic (Lines 55-95):
-
-```typescript
-const isPakistan = userCountry?.toUpperCase() === 'PK';
-
-// Available options based on country:
-const availableOptions: PaymentOption[] = isPakistan
- ? ['stripe', 'bank_transfer']
- : ['stripe', 'paypal'];
-```
-
-#### Stripe Payment (Lines 115-140):
-
-```typescript
-const handleStripePayment = async () => {
- const result = await subscribeToPlan(planIdentifier, '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;
-};
-```
-
-#### Bank Transfer Submission (Lines 203-250):
-
-```typescript
-const handleBankSubmit = async () => {
- await fetch(`${API_BASE_URL}/v1/billing/admin/payments/confirm/`, {
- method: 'POST',
- body: JSON.stringify({
- invoice_id: invoice.id,
- payment_method: 'bank_transfer',
- amount: invoice.total_amount,
- manual_reference: bankFormData.manual_reference,
- manual_notes: bankFormData.manual_notes,
- proof_url: bankFormData.proof_url,
- }),
- });
-};
-```
-
----
-
-### 4. PendingPaymentBanner.tsx
-
-**File:** [frontend/src/components/billing/PendingPaymentBanner.tsx](../../../frontend/src/components/billing/PendingPaymentBanner.tsx)
-
-**Purpose:** Alert banner displayed when account status is `pending_payment`.
-
-#### Key Features (Lines 1-338)
-
-```
-- Account Status Check: Lines 45-48
-- Pending Invoice Loading: Lines 60-130
-- Payment Gateway Detection: Lines 100-125
-- Pay Invoice Modal Trigger: Line 160
-```
-
-#### Display Logic:
-
-```typescript
-const accountStatus = user?.account?.status;
-const isPendingPayment = accountStatus === 'pending_payment';
-```
-
-#### Loads:
-
-1. Pending invoices from `/v1/billing/invoices/?status=pending&limit=1`
-2. Account payment methods from `/v1/billing/payment-methods/`
-3. Available gateways from `/v1/billing/stripe/config/` and `/v1/billing/paypal/config/`
-
----
-
-## Frontend Payment Services
-
-### billing.api.ts
-
-**File:** [frontend/src/services/billing.api.ts](../../../frontend/src/services/billing.api.ts)
-
-**Total Lines:** 1443
-
-#### Key Payment Functions:
-
-| Function | Lines | Endpoint | Description |
-|----------|-------|----------|-------------|
-| `getStripeConfig` | 1095-1097 | GET `/v1/billing/stripe/config/` | Get Stripe publishable key |
-| `createStripeCheckout` | 1102-1112 | POST `/v1/billing/stripe/checkout/` | Create checkout session |
-| `createStripeCreditCheckout` | 1200-1215 | POST `/v1/billing/stripe/credit-checkout/` | Credit package checkout |
-| `openStripeBillingPortal` | 1220-1230 | POST `/v1/billing/stripe/billing-portal/` | Subscription management |
-| `getPayPalConfig` | 1270-1272 | GET `/v1/billing/paypal/config/` | Get PayPal client ID |
-| `createPayPalCreditOrder` | 1278-1290 | POST `/v1/billing/paypal/create-order/` | Credit package order |
-| `createPayPalSubscriptionOrder` | 1295-1307 | POST `/v1/billing/paypal/create-subscription-order/` | Subscription order |
-| `capturePayPalOrder` | 1312-1325 | POST `/v1/billing/paypal/capture-order/` | Capture approved order |
-| `subscribeToPlan` | 1365-1380 | Gateway-specific | Unified subscribe helper |
-| `purchaseCredits` | 1385-1400 | Gateway-specific | Unified purchase helper |
-| `getInvoices` | 480-492 | GET `/v1/billing/invoices/` | List invoices |
-| `getPayments` | 510-522 | GET `/v1/billing/payments/` | List payments |
-| `submitManualPayment` | 528-542 | POST `/v1/billing/payments/manual/` | Submit bank transfer |
-| `getAvailablePaymentMethods` | 648-665 | GET `/v1/billing/payment-configs/payment-methods/` | Get payment configs |
-
-#### Helper Functions (Lines 1355-1443):
-
-```typescript
-// Subscribe to plan using preferred gateway
-export async function subscribeToPlan(
- planId: string,
- gateway: PaymentGateway,
- options?: { return_url?: string; cancel_url?: string }
-): Promise<{ redirect_url: string }> {
- switch (gateway) {
- case 'stripe': return await createStripeCheckout(planId, options);
- case 'paypal': return await createPayPalSubscriptionOrder(planId, options);
- case 'manual': throw new Error('Use submitManualPayment()');
- }
-}
-
-// Purchase credit package using preferred gateway
-export async function purchaseCredits(
- packageId: string,
- gateway: PaymentGateway,
- options?: { return_url?: string; cancel_url?: string }
-): Promise<{ redirect_url: string }> {
- switch (gateway) {
- case 'stripe': return await createStripeCreditCheckout(packageId, options);
- case 'paypal': return await createPayPalCreditOrder(packageId, options);
- case 'manual': throw new Error('Use submitManualPayment()');
- }
-}
-```
-
----
-
-### authStore.ts
-
-**File:** [frontend/src/store/authStore.ts](../../../frontend/src/store/authStore.ts)
-
-**Total Lines:** 534
-
-#### Register Function (Lines 230-395):
-
-```typescript
-register: async (registerData) => {
- const response = await fetch(`${API_BASE_URL}/v1/auth/register/`, {
- method: 'POST',
- body: JSON.stringify({
- ...registerData,
- password_confirm: registerData.password,
- plan_slug: registerData.plan_slug,
- }),
- });
-
- const data = await response.json();
-
- // Extract checkout_url for payment redirect
- return {
- ...userData,
- checkout_url: responseData.checkout_url || data.checkout_url,
- checkout_session_id: responseData.checkout_session_id,
- paypal_order_id: responseData.paypal_order_id,
- };
-}
-```
-
-**Key Return Fields:**
-- `checkout_url` - Stripe/PayPal redirect URL
-- `checkout_session_id` - Stripe session ID
-- `paypal_order_id` - PayPal order ID
-
----
-
-## Backend Endpoints
-
-### Billing URLs
-
-**File:** [backend/igny8_core/business/billing/urls.py](../../../backend/igny8_core/business/billing/urls.py)
-
-```python
-# Stripe Endpoints
-path('stripe/config/', StripeConfigView.as_view())
-path('stripe/checkout/', StripeCheckoutView.as_view())
-path('stripe/credit-checkout/', StripeCreditCheckoutView.as_view())
-path('stripe/billing-portal/', StripeBillingPortalView.as_view())
-path('webhooks/stripe/', stripe_webhook)
-
-# PayPal Endpoints
-path('paypal/config/', PayPalConfigView.as_view())
-path('paypal/create-order/', PayPalCreateOrderView.as_view())
-path('paypal/create-subscription-order/', PayPalCreateSubscriptionOrderView.as_view())
-path('paypal/capture-order/', PayPalCaptureOrderView.as_view())
-path('paypal/create-subscription/', PayPalCreateSubscriptionView.as_view())
-path('webhooks/paypal/', paypal_webhook)
-
-# Router ViewSets
-router.register(r'invoices', InvoiceViewSet)
-router.register(r'payments', PaymentViewSet)
-router.register(r'credit-packages', CreditPackageViewSet)
-router.register(r'payment-methods', AccountPaymentMethodViewSet)
-router.register(r'payment-configs', BillingViewSet)
-```
-
----
-
-### Stripe Views
-
-**File:** [backend/igny8_core/business/billing/views/stripe_views.py](../../../backend/igny8_core/business/billing/views/stripe_views.py)
-
-**Total Lines:** 802
-
-| View | Lines | Method | Endpoint | Description |
-|------|-------|--------|----------|-------------|
-| `StripeConfigView` | 35-55 | GET | `/stripe/config/` | Returns publishable key |
-| `StripeCheckoutView` | 58-125 | POST | `/stripe/checkout/` | Creates subscription checkout |
-| `StripeCreditCheckoutView` | 128-200 | POST | `/stripe/credit-checkout/` | Creates credit checkout |
-| `StripeBillingPortalView` | 203-260 | POST | `/stripe/billing-portal/` | Creates portal session |
-| `stripe_webhook` | 263-350 | POST | `/webhooks/stripe/` | Handles Stripe webhooks |
-
----
-
-### PayPal Views
-
-**File:** [backend/igny8_core/business/billing/views/paypal_views.py](../../../backend/igny8_core/business/billing/views/paypal_views.py)
-
-**Total Lines:** 910
-
-| View | Lines | Method | Endpoint | Description |
-|------|-------|--------|----------|-------------|
-| `PayPalConfigView` | 42-58 | GET | `/paypal/config/` | Returns client ID |
-| `PayPalCreateOrderView` | 61-135 | POST | `/paypal/create-order/` | Creates credit order |
-| `PayPalCreateSubscriptionOrderView` | 138-230 | POST | `/paypal/create-subscription-order/` | Creates subscription order |
-| `PayPalCaptureOrderView` | 233-400 | POST | `/paypal/capture-order/` | Captures approved order |
-| `PayPalCreateSubscriptionView` | 403-500 | POST | `/paypal/create-subscription/` | Creates recurring subscription |
-| `paypal_webhook` | 503-700 | POST | `/webhooks/paypal/` | Handles PayPal webhooks |
-
----
-
-### Auth Register Endpoint
-
-**File:** [backend/igny8_core/auth/urls.py](../../../backend/igny8_core/auth/urls.py) (Lines 40-170)
-
-**Also:** [backend/igny8_core/auth/serializers.py](../../../backend/igny8_core/auth/serializers.py) (Lines 280-500)
-
-#### RegisterSerializer.create() Flow (Lines 340-500):
-
-1. **Plan Lookup** (Lines 340-365):
- ```python
- if plan_slug in ['starter', 'growth', 'scale']:
- plan = Plan.objects.get(slug=plan_slug, is_active=True)
- account_status = 'pending_payment'
- initial_credits = 0
- else:
- plan = Plan.objects.get(slug='free', is_active=True)
- account_status = 'trial'
- initial_credits = plan.included_credits
- ```
-
-2. **Account Creation** (Lines 390-420):
- ```python
- account = Account.objects.create(
- name=account_name,
- owner=user,
- plan=plan,
- credits=initial_credits,
- status=account_status, # 'pending_payment' or 'trial'
- payment_method=validated_data.get('payment_method', 'bank_transfer'),
- billing_country=validated_data.get('billing_country', ''),
- )
- ```
-
-3. **For Paid Plans** (Lines 445-485):
- ```python
- # Create Subscription
- subscription = Subscription.objects.create(
- account=account,
- plan=plan,
- status='pending_payment',
- current_period_start=billing_period_start,
- current_period_end=billing_period_end,
- )
-
- # Create Invoice
- InvoiceService.create_subscription_invoice(subscription, ...)
-
- # Create AccountPaymentMethod
- AccountPaymentMethod.objects.create(
- account=account,
- type=payment_method,
- is_default=True,
- is_verified=False,
- )
- ```
-
-#### Checkout Session Creation (RegisterView - Lines 118-165):
-
-```python
-# For Stripe
-if payment_method == 'stripe':
- checkout_data = stripe_service.create_checkout_session(
- account=account,
- plan=account.plan,
- )
- response_data['checkout_url'] = checkout_data['checkout_url']
-
-# For PayPal
-elif payment_method == 'paypal':
- order = paypal_service.create_order(
- account=account,
- amount=float(account.plan.price),
- description=f'{account.plan.name} Plan Subscription',
- )
- response_data['checkout_url'] = order['approval_url']
-```
-
----
-
-## Backend Services
-
-### StripeService
-
-**File:** [backend/igny8_core/business/billing/services/stripe_service.py](../../../backend/igny8_core/business/billing/services/stripe_service.py)
-
-**Total Lines:** 628
-
-#### Key Methods:
-
-| Method | Lines | Description |
-|--------|-------|-------------|
-| `__init__` | 28-60 | Initialize from IntegrationProvider |
-| `_get_or_create_customer` | 73-110 | Get/create Stripe customer |
-| `create_checkout_session` | 130-210 | Create subscription checkout |
-| `create_credit_checkout_session` | 215-280 | Create credit package checkout |
-| `create_billing_portal_session` | 330-375 | Create portal session |
-| `get_subscription` | 380-420 | Get subscription details |
-| `cancel_subscription` | 425-450 | Cancel subscription |
-| `construct_webhook_event` | 485-500 | Verify webhook signature |
-| `create_refund` | 555-600 | Create refund |
-
-#### Checkout Session Flow:
-
-```python
-def create_checkout_session(self, account, plan, success_url=None, cancel_url=None):
- customer_id = self._get_or_create_customer(account)
-
- session = stripe.checkout.Session.create(
- customer=customer_id,
- payment_method_types=['card'],
- mode='subscription',
- line_items=[{'price': plan.stripe_price_id, 'quantity': 1}],
- success_url=success_url or f'{self.frontend_url}/account/plans?success=true',
- cancel_url=cancel_url or f'{self.frontend_url}/account/plans?canceled=true',
- metadata={
- 'account_id': str(account.id),
- 'plan_id': str(plan.id),
- 'type': 'subscription',
- },
- )
-
- return {'checkout_url': session.url, 'session_id': session.id}
-```
-
----
-
-### PayPalService
-
-**File:** [backend/igny8_core/business/billing/services/paypal_service.py](../../../backend/igny8_core/business/billing/services/paypal_service.py)
-
-**Total Lines:** 680
-
-#### Key Methods:
-
-| Method | Lines | Description |
-|--------|-------|-------------|
-| `__init__` | 44-85 | Initialize from IntegrationProvider |
-| `_get_access_token` | 105-140 | Get OAuth token |
-| `_make_request` | 145-195 | Make authenticated request |
-| `create_order` | 220-275 | Create one-time order |
-| `create_credit_order` | 300-340 | Create credit package order |
-| `capture_order` | 345-400 | Capture approved order |
-| `create_subscription` | 420-480 | Create recurring subscription |
-| `cancel_subscription` | 510-535 | Cancel subscription |
-| `verify_webhook_signature` | 565-610 | Verify webhook |
-| `refund_capture` | 615-660 | Refund payment |
-
-#### Order Flow:
-
-```python
-def create_order(self, account, amount, description='', return_url=None, cancel_url=None):
- order_data = {
- 'intent': 'CAPTURE',
- 'purchase_units': [{
- 'amount': {'currency_code': self.currency, 'value': f'{amount:.2f}'},
- 'description': description or 'IGNY8 Payment',
- 'custom_id': str(account.id),
- }],
- 'application_context': {
- 'return_url': return_url or self.return_url,
- 'cancel_url': cancel_url or self.cancel_url,
- 'brand_name': 'IGNY8',
- 'user_action': 'PAY_NOW',
- }
- }
-
- response = self._make_request('POST', '/v2/checkout/orders', json_data=order_data)
-
- return {
- 'order_id': response.get('id'),
- 'status': response.get('status'),
- 'approval_url': approval_url, # From links
- }
-```
-
----
-
-### InvoiceService
-
-**File:** [backend/igny8_core/business/billing/services/invoice_service.py](../../../backend/igny8_core/business/billing/services/invoice_service.py)
-
-**Total Lines:** 385
-
-#### Key Methods:
-
-| Method | Lines | Description |
-|--------|-------|-------------|
-| `get_pending_invoice` | 18-27 | Get pending invoice for subscription |
-| `get_or_create_subscription_invoice` | 30-50 | Avoid duplicate invoices |
-| `generate_invoice_number` | 53-80 | Generate unique invoice number |
-| `create_subscription_invoice` | 85-165 | Create subscription invoice |
-| `create_credit_package_invoice` | 170-230 | Create credit package invoice |
-| `mark_paid` | 260-290 | Mark invoice as paid |
-| `mark_void` | 295-310 | Void invoice |
-
-#### Currency Logic:
-
-```python
-# Online payments (stripe, paypal): Always USD
-# Manual payments (bank_transfer, local_wallet): Local currency
-
-if payment_method in ['stripe', 'paypal']:
- currency = 'USD'
- local_price = float(plan.price)
-else:
- currency = get_currency_for_country(account.billing_country) # 'PKR' for PK
- local_price = convert_usd_to_local(float(plan.price), account.billing_country)
-```
-
----
-
-### PaymentService
-
-**File:** [backend/igny8_core/business/billing/services/payment_service.py](../../../backend/igny8_core/business/billing/services/payment_service.py)
-
-**Total Lines:** 418
-
-#### Key Methods:
-
-| Method | Lines | Description |
-|--------|-------|-------------|
-| `create_stripe_payment` | 17-35 | Record Stripe payment |
-| `create_paypal_payment` | 40-55 | Record PayPal payment |
-| `create_manual_payment` | 60-92 | Record manual payment (pending_approval) |
-| `mark_payment_completed` | 97-130 | Mark as succeeded |
-| `mark_payment_failed` | 135-150 | Mark as failed |
-| `approve_manual_payment` | 155-200 | Admin approves manual payment |
-| `reject_manual_payment` | 205-230 | Admin rejects manual payment |
-| `_add_credits_for_payment` | 235-270 | Add credits after payment |
-| `get_available_payment_methods` | 275-340 | Get methods by country |
-
-#### Manual Payment Approval:
-
-```python
-def approve_manual_payment(payment, approved_by_user_id, admin_notes=None):
- payment.status = 'succeeded'
- payment.approved_by_id = approved_by_user_id
- payment.approved_at = timezone.now()
- payment.save()
-
- # Update invoice
- InvoiceService.mark_paid(payment.invoice, payment_method=payment.payment_method)
-
- # Add credits if credit package
- if payment.metadata.get('credit_package_id'):
- PaymentService._add_credits_for_payment(payment)
-
- # Activate account
- if account.status != 'active':
- account.status = 'active'
- account.save()
-```
-
----
-
-## Models
-
-### Account Model
-
-**File:** [backend/igny8_core/auth/models.py](../../../backend/igny8_core/auth/models.py) (Lines 55-145)
-
-```python
-class Account(SoftDeletableModel):
- STATUS_CHOICES = [
- ('active', 'Active'),
- ('suspended', 'Suspended'),
- ('trial', 'Trial'),
- ('cancelled', 'Cancelled'),
- ('pending_payment', 'Pending Payment'),
- ]
-
- PAYMENT_METHOD_CHOICES = [
- ('stripe', 'Stripe'),
- ('paypal', 'PayPal'),
- ('bank_transfer', 'Bank Transfer'),
- ]
-
- name = models.CharField(max_length=255)
- slug = models.SlugField(unique=True)
- owner = models.ForeignKey('User', on_delete=models.SET_NULL, null=True)
- stripe_customer_id = models.CharField(max_length=255, blank=True, null=True)
- plan = models.ForeignKey('Plan', on_delete=models.PROTECT)
- credits = models.IntegerField(default=0)
- status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='trial')
- payment_method = models.CharField(max_length=30, choices=PAYMENT_METHOD_CHOICES, default='stripe')
-
- # Billing information
- billing_email = models.EmailField(blank=True, null=True)
- billing_address_line1 = models.CharField(max_length=255, blank=True)
- billing_country = models.CharField(max_length=2, blank=True) # ISO country code
- tax_id = models.CharField(max_length=100, blank=True)
-```
-
----
-
-### Subscription Model
-
-**File:** [backend/igny8_core/auth/models.py](../../../backend/igny8_core/auth/models.py) (Lines 395-440)
-
-```python
-class Subscription(models.Model):
- STATUS_CHOICES = [
- ('active', 'Active'),
- ('past_due', 'Past Due'),
- ('canceled', 'Canceled'),
- ('trialing', 'Trialing'),
- ('pending_payment', 'Pending Payment'),
- ]
-
- account = models.OneToOneField('Account', on_delete=models.CASCADE, related_name='subscription')
- plan = models.ForeignKey('Plan', on_delete=models.PROTECT)
- stripe_subscription_id = models.CharField(max_length=255, blank=True, null=True)
- external_payment_id = models.CharField(max_length=255, blank=True, null=True) # PayPal/Bank ref
- status = models.CharField(max_length=20, choices=STATUS_CHOICES)
- current_period_start = models.DateTimeField()
- current_period_end = models.DateTimeField()
- cancel_at_period_end = models.BooleanField(default=False)
-```
-
----
-
-### Plan Model
-
-**File:** [backend/igny8_core/auth/models.py](../../../backend/igny8_core/auth/models.py) (Lines 300-395)
-
-```python
-class Plan(models.Model):
- name = models.CharField(max_length=255)
- slug = models.SlugField(unique=True)
- price = models.DecimalField(max_digits=10, decimal_places=2)
- billing_cycle = models.CharField(max_length=20, choices=[('monthly', 'Monthly'), ('annual', 'Annual')])
- is_active = models.BooleanField(default=True)
- is_internal = models.BooleanField(default=False) # Hidden from public (Free plan)
-
- # Limits
- max_users = models.IntegerField(default=1)
- max_sites = models.IntegerField(default=1)
- max_keywords = models.IntegerField(default=1000)
-
- # Credits
- included_credits = models.IntegerField(default=0)
-
- # Payment Integration
- stripe_product_id = models.CharField(max_length=255, blank=True, null=True)
- stripe_price_id = models.CharField(max_length=255, blank=True, null=True)
-```
-
----
-
-### Invoice Model
-
-**File:** [backend/igny8_core/business/billing/models.py](../../../backend/igny8_core/business/billing/models.py) (Lines 310-380)
-
-```python
-class Invoice(AccountBaseModel):
- STATUS_CHOICES = [
- ('draft', 'Draft'),
- ('pending', 'Pending'),
- ('paid', 'Paid'),
- ('void', 'Void'),
- ('uncollectible', 'Uncollectible'),
- ]
-
- invoice_number = models.CharField(max_length=50, unique=True)
- subscription = models.ForeignKey('Subscription', on_delete=models.SET_NULL, null=True)
- subtotal = models.DecimalField(max_digits=10, decimal_places=2, default=0)
- tax = models.DecimalField(max_digits=10, decimal_places=2, default=0)
- total = models.DecimalField(max_digits=10, decimal_places=2, default=0)
- currency = models.CharField(max_length=3, default='USD')
- status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
- invoice_date = models.DateField()
- due_date = models.DateField()
- paid_at = models.DateTimeField(null=True, blank=True)
- line_items = models.JSONField(default=list)
- stripe_invoice_id = models.CharField(max_length=255, null=True, blank=True)
- payment_method = models.CharField(max_length=50, null=True, blank=True)
- metadata = models.JSONField(default=dict)
-```
-
----
-
-### Payment Model
-
-**File:** [backend/igny8_core/business/billing/models.py](../../../backend/igny8_core/business/billing/models.py) (Lines 460-540)
-
-```python
-class Payment(AccountBaseModel):
- STATUS_CHOICES = [
- ('pending_approval', 'Pending Approval'), # Manual payment awaiting admin
- ('succeeded', 'Succeeded'),
- ('failed', 'Failed'),
- ('refunded', 'Refunded'),
- ]
-
- PAYMENT_METHOD_CHOICES = [
- ('stripe', 'Stripe'),
- ('paypal', 'PayPal'),
- ('bank_transfer', 'Bank Transfer'),
- ('local_wallet', 'Local Wallet'),
- ('manual', 'Manual Payment'),
- ]
-
- invoice = models.ForeignKey(Invoice, on_delete=models.CASCADE)
- amount = models.DecimalField(max_digits=10, decimal_places=2)
- currency = models.CharField(max_length=3, default='USD')
- status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending_approval')
- payment_method = models.CharField(max_length=50, choices=PAYMENT_METHOD_CHOICES)
-
- # Stripe
- stripe_payment_intent_id = models.CharField(max_length=255, null=True, blank=True)
- stripe_charge_id = models.CharField(max_length=255, null=True, blank=True)
-
- # PayPal
- paypal_order_id = models.CharField(max_length=255, null=True, blank=True)
- paypal_capture_id = models.CharField(max_length=255, null=True, blank=True)
-
- # Manual
- manual_reference = models.CharField(max_length=255, blank=True) # Bank transfer ref
- manual_notes = models.TextField(blank=True)
- admin_notes = models.TextField(blank=True)
- approved_by = models.ForeignKey('User', null=True, blank=True, on_delete=models.SET_NULL)
- approved_at = models.DateTimeField(null=True, blank=True)
-
- metadata = models.JSONField(default=dict)
-```
-
----
-
-### AccountPaymentMethod Model
-
-**File:** [backend/igny8_core/business/billing/models.py](../../../backend/igny8_core/business/billing/models.py) (Lines 700-750)
-
-```python
-class AccountPaymentMethod(AccountBaseModel):
- """Account-scoped payment methods (user's verified payment options)"""
- PAYMENT_METHOD_CHOICES = [
- ('stripe', 'Stripe'),
- ('paypal', 'PayPal'),
- ('bank_transfer', 'Bank Transfer'),
- ('local_wallet', 'Local Wallet'),
- ('manual', 'Manual Payment'),
- ]
-
- type = models.CharField(max_length=50, choices=PAYMENT_METHOD_CHOICES)
- display_name = models.CharField(max_length=100)
- is_default = models.BooleanField(default=False)
- is_enabled = models.BooleanField(default=True)
- is_verified = models.BooleanField(default=False) # True after successful payment
- country_code = models.CharField(max_length=2, blank=True)
- instructions = models.TextField(blank=True)
- metadata = models.JSONField(default=dict)
-```
-
----
-
-### PaymentMethodConfig Model
-
-**File:** [backend/igny8_core/business/billing/models.py](../../../backend/igny8_core/business/billing/models.py) (Lines 600-695)
-
-```python
-class PaymentMethodConfig(models.Model):
- """System-level payment method configuration per country"""
- PAYMENT_METHOD_CHOICES = [
- ('stripe', 'Stripe'),
- ('paypal', 'PayPal'),
- ('bank_transfer', 'Bank Transfer'),
- ('local_wallet', 'Local Wallet'),
- ('manual', 'Manual Payment'),
- ]
-
- country_code = models.CharField(max_length=2) # 'US', 'PK', '*' for global
- payment_method = models.CharField(max_length=50, choices=PAYMENT_METHOD_CHOICES)
- is_enabled = models.BooleanField(default=True)
- display_name = models.CharField(max_length=100, blank=True)
- instructions = models.TextField(blank=True)
-
- # Bank Transfer Details
- bank_name = models.CharField(max_length=255, blank=True)
- account_number = models.CharField(max_length=255, blank=True)
- account_title = models.CharField(max_length=255, blank=True)
- routing_number = models.CharField(max_length=255, blank=True)
- swift_code = models.CharField(max_length=255, blank=True)
- iban = models.CharField(max_length=255, blank=True)
-
- # Local Wallet Details
- wallet_type = models.CharField(max_length=100, blank=True) # JazzCash, EasyPaisa
- wallet_id = models.CharField(max_length=255, blank=True)
-
- sort_order = models.IntegerField(default=0)
-
- class Meta:
- unique_together = [['country_code', 'payment_method']]
-```
-
----
-
-## Payment Method Configuration
-
-### Country-Based Payment Methods
-
-| Country | Stripe | PayPal | Bank Transfer | Local Wallet |
-|---------|--------|--------|---------------|--------------|
-| Global (`*`) | β
| β
| β | β |
-| Pakistan (`PK`) | β
| β | β
| β
(JazzCash, EasyPaisa) |
-
-### Configuration Flow
-
-1. **PaymentMethodConfig** stores system-level configs per country
-2. **AccountPaymentMethod** stores user's saved/verified methods
-3. Frontend queries `/v1/billing/payment-configs/payment-methods/` to get available options
-4. Frontend filters based on user's country (from `countryCode` prop or account's `billing_country`)
-
----
-
-## Payment Flows
-
-### Flow 1: Signup with Stripe
-
-```mermaid
-sequenceDiagram
- participant U as User
- participant F as Frontend
- participant B as Backend
- participant S as Stripe
-
- U->>F: Fill signup form, select Stripe
- F->>B: POST /v1/auth/register/ {payment_method: 'stripe', plan_slug: 'starter'}
- B->>B: Create User, Account (status=pending_payment), Subscription, Invoice
- B->>S: Create Checkout Session
- S-->>B: checkout_url
- B-->>F: {user, tokens, checkout_url}
- F->>S: Redirect to checkout_url
- U->>S: Complete payment
- S->>B: POST /webhooks/stripe/ (checkout.session.completed)
- B->>B: Activate subscription, mark invoice paid, add credits
- S->>F: Redirect to success_url
- F->>B: GET /v1/auth/me/ (refresh user)
-```
-
-### Flow 2: Signup with Bank Transfer
-
-```mermaid
-sequenceDiagram
- participant U as User
- participant F as Frontend
- participant B as Backend
- participant A as Admin
-
- U->>F: Fill signup form, select Bank Transfer
- F->>B: POST /v1/auth/register/ {payment_method: 'bank_transfer'}
- B->>B: Create User, Account (status=pending_payment), Subscription, Invoice, AccountPaymentMethod
- B-->>F: {user, tokens}
- F->>F: Navigate to /account/plans (shows PendingPaymentBanner)
- U->>F: Make bank transfer, enter reference
- F->>B: POST /v1/billing/admin/payments/confirm/ {invoice_id, manual_reference}
- B->>B: Create Payment (status=pending_approval)
- A->>B: POST /v1/admin/billing/{id}/approve_payment/
- B->>B: Mark payment succeeded, invoice paid, activate account, add credits
-```
-
-### Flow 3: Credit Purchase with PayPal
-
-```mermaid
-sequenceDiagram
- participant U as User
- participant F as Frontend
- participant B as Backend
- participant P as PayPal
-
- U->>F: Click Buy Credits, select PayPal
- F->>B: POST /v1/billing/paypal/create-order/ {package_id}
- B->>P: Create Order
- P-->>B: {order_id, approval_url}
- B-->>F: {order_id, approval_url}
- F->>P: Redirect to approval_url
- U->>P: Approve payment
- P->>F: Redirect to return_url with token
- F->>B: POST /v1/billing/paypal/capture-order/ {order_id}
- B->>P: Capture Order
- P-->>B: {capture_id, status: COMPLETED}
- B->>B: Create invoice, payment, add credits
- B-->>F: {credits_added, new_balance}
-```
-
-### Flow 4: Pay Pending Invoice (PayInvoiceModal)
-
-```mermaid
-sequenceDiagram
- participant U as User
- participant F as Frontend
- participant B as Backend
- participant S as Stripe
-
- U->>F: Click "Pay Invoice" button
- F->>F: Open PayInvoiceModal
- U->>F: Select Stripe, click Pay
- F->>B: POST /v1/billing/stripe/checkout/ {plan_id}
- B->>S: Create Checkout Session
- S-->>B: checkout_url
- B-->>F: {checkout_url}
- F->>S: Redirect to checkout_url
- U->>S: Complete payment
- S->>B: Webhook: checkout.session.completed
- B->>B: Update subscription, invoice, account status
-```
-
----
-
-## Webhooks
-
-### Stripe Webhooks
-
-**Endpoint:** `POST /v1/billing/webhooks/stripe/`
-
-| Event | Handler | Action |
-|-------|---------|--------|
-| `checkout.session.completed` | `_handle_checkout_completed` | Activate subscription OR add purchased credits |
-| `invoice.paid` | `_handle_invoice_paid` | Add monthly credits for renewal |
-| `invoice.payment_failed` | `_handle_payment_failed` | Send notification, update status |
-| `customer.subscription.updated` | `_handle_subscription_updated` | Sync plan changes |
-| `customer.subscription.deleted` | `_handle_subscription_deleted` | Cancel subscription |
-
-#### Checkout Completed Handler (Lines 355-475):
-
-```python
-def _handle_checkout_completed(session):
- metadata = session.get('metadata', {})
- account_id = metadata.get('account_id')
- mode = session.get('mode')
-
- if mode == 'subscription':
- _activate_subscription(account, subscription_id, plan_id, session)
- elif mode == 'payment':
- _add_purchased_credits(account, credit_package_id, credit_amount, session)
-```
-
-### PayPal Webhooks
-
-**Endpoint:** `POST /v1/billing/webhooks/paypal/`
-
-| Event | Action |
-|-------|--------|
-| `CHECKOUT.ORDER.APPROVED` | Auto-capture (if configured) |
-| `PAYMENT.CAPTURE.COMPLETED` | Mark payment succeeded, add credits |
-| `PAYMENT.CAPTURE.DENIED` | Mark payment failed |
-| `BILLING.SUBSCRIPTION.ACTIVATED` | Activate subscription |
-| `BILLING.SUBSCRIPTION.CANCELLED` | Cancel subscription |
-| `BILLING.SUBSCRIPTION.PAYMENT.FAILED` | Handle failed renewal |
-
----
-
-## Summary
-
-### Key Files Reference
-
-| Category | File | Lines |
-|----------|------|-------|
-| **Frontend Entry Points** | | |
-| Signup Form | `frontend/src/components/auth/SignUpFormUnified.tsx` | 770 |
-| Plans & Billing Page | `frontend/src/pages/account/PlansAndBillingPage.tsx` | 1197 |
-| Pay Invoice Modal | `frontend/src/components/billing/PayInvoiceModal.tsx` | 544 |
-| Pending Payment Banner | `frontend/src/components/billing/PendingPaymentBanner.tsx` | 338 |
-| **Frontend Services** | | |
-| Billing API | `frontend/src/services/billing.api.ts` | 1443 |
-| Auth Store | `frontend/src/store/authStore.ts` | 534 |
-| **Backend Views** | | |
-| Stripe Views | `backend/igny8_core/business/billing/views/stripe_views.py` | 802 |
-| PayPal Views | `backend/igny8_core/business/billing/views/paypal_views.py` | 910 |
-| **Backend Services** | | |
-| Stripe Service | `backend/igny8_core/business/billing/services/stripe_service.py` | 628 |
-| PayPal Service | `backend/igny8_core/business/billing/services/paypal_service.py` | 680 |
-| Invoice Service | `backend/igny8_core/business/billing/services/invoice_service.py` | 385 |
-| Payment Service | `backend/igny8_core/business/billing/services/payment_service.py` | 418 |
-| **Models** | | |
-| Auth Models | `backend/igny8_core/auth/models.py` | 866 |
-| Billing Models | `backend/igny8_core/business/billing/models.py` | 857 |
-| **Serializers** | | |
-| Auth Serializers | `backend/igny8_core/auth/serializers.py` | 561 |
-| **URLs** | | |
-| Billing URLs | `backend/igny8_core/business/billing/urls.py` | 73 |
-| Auth URLs | `backend/igny8_core/auth/urls.py` | 461 |
-
----
-
-## Quick Reference: API Endpoints
-
-### Stripe
-
-```
-GET /v1/billing/stripe/config/ - Get publishable key
-POST /v1/billing/stripe/checkout/ - Create subscription checkout
-POST /v1/billing/stripe/credit-checkout/ - Create credit checkout
-POST /v1/billing/stripe/billing-portal/ - Open billing portal
-POST /v1/billing/webhooks/stripe/ - Webhook handler
-```
-
-### PayPal
-
-```
-GET /v1/billing/paypal/config/ - Get client ID
-POST /v1/billing/paypal/create-order/ - Create credit order
-POST /v1/billing/paypal/create-subscription-order/ - Create subscription order
-POST /v1/billing/paypal/capture-order/ - Capture approved order
-POST /v1/billing/paypal/create-subscription/ - Create recurring subscription
-POST /v1/billing/webhooks/paypal/ - Webhook handler
-```
-
-### Invoices & Payments
-
-```
-GET /v1/billing/invoices/ - List invoices
-GET /v1/billing/invoices/{id}/ - Get invoice detail
-GET /v1/billing/invoices/{id}/download_pdf/ - Download PDF
-GET /v1/billing/payments/ - List payments
-POST /v1/billing/payments/manual/ - Submit manual payment
-```
-
-### Admin Payment Management
-
-```
-GET /v1/admin/billing/pending_payments/ - List pending approvals
-POST /v1/admin/billing/{id}/approve_payment/ - Approve payment
-POST /v1/admin/billing/{id}/reject_payment/ - Reject payment
-```
-
-### Payment Methods
-
-```
-GET /v1/billing/payment-methods/ - Get user's payment methods
-POST /v1/billing/payment-methods/ - Create payment method
-POST /v1/billing/payment-methods/{id}/set_default/ - Set default
-GET /v1/billing/payment-configs/payment-methods/ - Get available configs
-```
diff --git a/docs/90-REFERENCE/PAYMENT-SYSTEM-AUDIT-REPORT.md b/docs/90-REFERENCE/PAYMENT-SYSTEM-AUDIT-REPORT.md
deleted file mode 100644
index ff2321e9..00000000
--- a/docs/90-REFERENCE/PAYMENT-SYSTEM-AUDIT-REPORT.md
+++ /dev/null
@@ -1,959 +0,0 @@
-# Payment System Audit Report
-
-> **Audit Date:** January 7, 2026
-> **Status:** Complete (Updated with Frontend UX Audit)
-> **Severity Levels:** CRITICAL | HIGH | MEDIUM | LOW
-
----
-
-## Executive Summary
-
-The IGNY8 payment system documentation is **accurate and matches the implementation**. However, the deep audit revealed **45+ issues** across security, reliability, UX, and functionality areas. The most critical concerns involve:
-
-1. **PayPal webhook signature verification disabled** (security risk)
-2. **Missing idempotency in payment processing** (double-charge risk)
-3. **No admin dashboard for manual payment approval** (operational gap)
-4. **Plan shows "Active" even with unpaid invoice** (misleading UX)
-5. **Payment options not properly restricted by state** (UX confusion)
-6. **Hardcoded currency exchange rates** (financial accuracy)
-7. **Refund functions reference non-existent modules** (broken feature)
-
----
-
-## Documentation Verification
-
-### All Documented Files: VERIFIED
-
-| Category | File | Status |
-|----------|------|--------|
-| Frontend Entry Points | SignUpFormUnified.tsx | EXISTS |
-| | PlansAndBillingPage.tsx | EXISTS |
-| | PayInvoiceModal.tsx | EXISTS |
-| | PendingPaymentBanner.tsx | EXISTS |
-| Frontend Services | billing.api.ts | EXISTS |
-| | authStore.ts | EXISTS |
-| Backend Views | stripe_views.py | EXISTS |
-| | paypal_views.py | EXISTS |
-| Backend Services | stripe_service.py | EXISTS |
-| | paypal_service.py | EXISTS |
-| | payment_service.py | EXISTS |
-| | invoice_service.py | EXISTS |
-| Models | billing/models.py | EXISTS |
-| | auth/models.py | EXISTS |
-
-### Country-Based Payment Logic: CORRECT
-
-- **Pakistan (PK):** Stripe + Bank Transfer (NO PayPal)
-- **Global:** Stripe + PayPal
-
-Logic correctly implemented in:
-- `SignUpFormUnified.tsx:160-186`
-- `PayInvoiceModal.tsx:69-97`
-- `payment_service.py:260-263`
-
----
-
-## PlansAndBillingPage UX Audit (NEW)
-
-### Overview
-
-The `/account/plans` page (`PlansAndBillingPage.tsx`) is the central hub for subscription management. This audit identifies **critical UX issues** related to state handling, invoice lifecycle, and payment option restrictions.
-
----
-
-### CRITICAL UX Issue #1: Plan Shows "Active" Even With Unpaid Invoice
-
-**Location:** `PlansAndBillingPage.tsx:459-461`
-
-**Current Code:**
-```tsx
-
- {hasActivePlan ? 'Active' : 'Inactive'}
-
-```
-
-**Problem:**
-- `hasActivePlan` is `true` if user has ANY plan assigned (Line 384)
-- User who signed up for paid plan but never completed payment sees **"Active"**
-- This is misleading - their account is actually `pending_payment`
-
-**Correct Logic Should Check:**
-1. `account.status === 'active'` (not just plan existence)
-2. No pending invoices
-3. Subscription status is `active` (not `pending_payment`)
-
-**Fix Required:**
-```tsx
-const accountStatus = user?.account?.status;
-const subscriptionStatus = currentSubscription?.status;
-const isFullyActive = accountStatus === 'active' &&
- subscriptionStatus === 'active' &&
- !hasPendingInvoice;
-
-
- {isFullyActive ? 'Active' : accountStatus === 'pending_payment' ? 'Pending Payment' : 'Inactive'}
-
-```
-
----
-
-### CRITICAL UX Issue #2: Subscription States Not Properly Reflected
-
-**Location:** `PlansAndBillingPage.tsx:379-386`
-
-**Current Code:**
-```tsx
-const currentSubscription = subscriptions.find((sub) => sub.status === 'active') || subscriptions[0];
-// ...
-const hasActivePlan = Boolean(effectivePlanId);
-```
-
-**Problem:**
-The page doesn't distinguish between subscription statuses:
-- `active` - Paid and working
-- `pending_payment` - Waiting for first payment
-- `past_due` - Renewal payment failed
-- `canceled` - User cancelled
-
-**Missing Status Handling:**
-
-| Subscription Status | What User Sees | What They SHOULD See |
-|---------------------|----------------|----------------------|
-| `pending_payment` | "Active" badge | "Payment Required" with prominent CTA |
-| `past_due` | No indication | "Payment Overdue" warning |
-| `canceled` | May still show "Active" | "Cancels on [date]" |
-| `trialing` | "Active" | "Trial (X days left)" |
-
-**Fix Required:** Add comprehensive status display:
-```tsx
-const getSubscriptionDisplay = () => {
- if (!currentSubscription) return { label: 'No Plan', tone: 'error' };
-
- switch (currentSubscription.status) {
- case 'active':
- return hasPendingInvoice
- ? { label: 'Payment Due', tone: 'warning' }
- : { label: 'Active', tone: 'success' };
- case 'pending_payment':
- return { label: 'Awaiting Payment', tone: 'warning' };
- case 'past_due':
- return { label: 'Payment Overdue', tone: 'error' };
- case 'canceled':
- return { label: `Cancels ${formatDate(currentSubscription.cancel_at)}`, tone: 'warning' };
- case 'trialing':
- return { label: `Trial`, tone: 'info' };
- default:
- return { label: currentSubscription.status, tone: 'neutral' };
- }
-};
-```
-
----
-
-### HIGH UX Issue #3: Upgrade Button Available When Payment Pending
-
-**Location:** `PlansAndBillingPage.tsx:478-486`
-
-**Current Code:**
-```tsx
- setShowUpgradeModal(true)}
- startIcon={ }
->
- Upgrade
-
-```
-
-**Problem:** User with pending invoice can click "Upgrade" and attempt to subscribe to another plan, creating confusion and potentially duplicate subscriptions.
-
-**Fix Required:**
-```tsx
- setShowUpgradeModal(true)}
- disabled={hasPendingInvoice || accountStatus === 'pending_payment'}
- startIcon={ }
->
- {hasPendingInvoice ? 'Pay Invoice First' : 'Upgrade'}
-
-```
-
----
-
-### HIGH UX Issue #4: Cancel Subscription Available When Account Already Pending
-
-**Location:** `PlansAndBillingPage.tsx:609-616`
-
-**Current Code:**
-```tsx
-{hasActivePlan && (
- setShowCancelConfirm(true)} ...>
- Cancel Subscription
-
-)}
-```
-
-**Problem:** User with `pending_payment` status can "cancel" a subscription they never paid for. This is confusing.
-
-**Fix Required:**
-```tsx
-{hasActivePlan && accountStatus === 'active' && !hasPendingInvoice && (
- setShowCancelConfirm(true)} ...>
- Cancel Subscription
-
-)}
-```
-
----
-
-### HIGH UX Issue #5: "Manage Billing" Button Shown to Non-Stripe Users
-
-**Location:** `PlansAndBillingPage.tsx:468-477`
-
-**Current Code:**
-```tsx
-{availableGateways.stripe && hasActivePlan && (
-
- Manage Billing
-
-)}
-```
-
-**Problem:**
-- Shows "Manage Billing" if Stripe is available, even if user pays via Bank Transfer
-- Bank Transfer users clicking this get error "No Stripe customer ID"
-
-**Fix Required:**
-```tsx
-{availableGateways.stripe &&
- hasActivePlan &&
- user?.account?.stripe_customer_id &&
- selectedPaymentMethod === 'stripe' && (
-
- Manage Billing
-
-)}
-```
-
----
-
-### HIGH UX Issue #6: Credits Section Doesn't Show Pending Credit Purchases
-
-**Location:** `PlansAndBillingPage.tsx:689-713`
-
-**Problem:** If user purchased credits via bank transfer and it's `pending_approval`, they don't see this anywhere clearly. They might try to purchase again.
-
-**Fix Required:** Add pending credits indicator:
-```tsx
-{pendingCreditPayments.length > 0 && (
-
-
- You have {pendingCreditPayments.length} credit purchase(s) pending approval
-
-
-)}
-```
-
----
-
-### MEDIUM UX Issue #7: Invoice Status Badge Colors Inconsistent
-
-**Location:** `PlansAndBillingPage.tsx:817-819`
-
-**Current Code:**
-```tsx
-
- {invoice.status}
-
-```
-
-**Problem:** Only handles `paid` and everything else is `warning`. Missing:
-- `draft` - Should be gray/neutral
-- `void` - Should be gray
-- `uncollectible` - Should be error/red
-- `pending` - Warning (correct)
-
-**Fix Required:**
-```tsx
-const getInvoiceStatusTone = (status: string) => {
- switch (status) {
- case 'paid': return 'success';
- case 'pending': return 'warning';
- case 'void':
- case 'draft': return 'neutral';
- case 'uncollectible': return 'error';
- default: return 'neutral';
- }
-};
-```
-
----
-
-### MEDIUM UX Issue #8: No Clear Indication of Payment Method per Invoice
-
-**Location:** `PlansAndBillingPage.tsx:809-849` (Invoice table)
-
-**Problem:** Invoice table doesn't show which payment method was used/expected. User can't tell if they need to do bank transfer or card payment.
-
-**Fix Required:** Add payment method column:
-```tsx
-
- {invoice.payment_method === 'bank_transfer' ? (
- Bank
- ) : invoice.payment_method === 'paypal' ? (
- PayPal
- ) : (
- Card
- )}
-
-```
-
----
-
-### MEDIUM UX Issue #9: Renewal Date Shows Even When No Active Subscription
-
-**Location:** `PlansAndBillingPage.tsx:514-522`
-
-**Current Code:**
-```tsx
-
- {currentSubscription?.current_period_end
- ? new Date(currentSubscription.current_period_end).toLocaleDateString(...)
- : 'β'}
-
-Next billing
-```
-
-**Problem:** Shows "β" and "Next billing" even when:
-- Account is `pending_payment` (never billed yet)
-- Subscription is `canceled` (won't renew)
-
-**Fix Required:**
-```tsx
-
- {currentSubscription?.status === 'canceled' ? 'Ends on' :
- currentSubscription?.status === 'pending_payment' ? 'Starts after payment' :
- 'Next billing'}
-
-```
-
----
-
-### MEDIUM UX Issue #10: Payment Gateway Selection Not Synced With Account
-
-**Location:** `PlansAndBillingPage.tsx:655-686`
-
-**Problem:** Payment method selector in "Buy Credits" section can show options the user hasn't verified. If user signed up with bank transfer, they shouldn't see PayPal as an option until they've added it.
-
-**Current Logic:** Shows all `availableGateways` regardless of user's `AccountPaymentMethod` records.
-
-**Fix Required:**
-```tsx
-// Only show gateways user has verified OR is willing to add
-const userCanUseGateway = (gateway: PaymentGateway) => {
- const userHasMethod = userPaymentMethods.some(m =>
- (gateway === 'stripe' && m.type === 'stripe') ||
- (gateway === 'paypal' && m.type === 'paypal') ||
- (gateway === 'manual' && ['bank_transfer', 'local_wallet'].includes(m.type))
- );
- return availableGateways[gateway] && (userHasMethod || gateway === selectedGateway);
-};
-```
-
----
-
-### LOW UX Issue #11: Annual Billing Toggle Does Nothing
-
-**Location:** `PlansAndBillingPage.tsx:959-983`
-
-**Current Code:**
-```tsx
-const displayPrice = selectedBillingCycle === 'annual' ? (annualPrice / 12).toFixed(0) : planPrice;
-```
-
-**Problem:**
-- Shows "Save 20%" badge for annual
-- Calculates display price
-- But **no annual plans exist** in database
-- Clicking "Choose Plan" subscribes to monthly regardless
-
-**Fix Required:** Either:
-1. Remove annual toggle until annual plans implemented
-2. Implement annual plan variants in backend
-3. Pass `billing_cycle` to `subscribeToPlan()` and handle in backend
-
----
-
-### LOW UX Issue #12: PayInvoiceModal Hardcodes Bank Details
-
-**Location:** `PayInvoiceModal.tsx:437-443`
-
-**Current Code:**
-```tsx
-Bank: Standard Chartered Bank Pakistan
-Account Title: IGNY8 Technologies
-Account #: 01-2345678-01
-```
-
-**Problem:** Bank details hardcoded in frontend. Should come from `PaymentMethodConfig` in backend.
-
-**Fix Required:** Fetch bank details from `/v1/billing/payment-configs/payment-methods/?country_code=PK&payment_method=bank_transfer` and display dynamically.
-
----
-
-### Account Lifecycle State Machine (Missing)
-
-The page doesn't follow a clear state machine. Here's what it SHOULD be:
-
-```
-βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-β ACCOUNT STATES β
-βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
-β β
-β ββββββββββββ Payment ββββββββββββ Payment ββββββββββββ β
-β β trial β βββββββββββββΆ β pending_ β βββββββββββββΆ β active β β
-β β β Required β payment β Success β β β
-β ββββββββββββ ββββββββββββ ββββββββββββ β
-β β β β β
-β β β Payment β β
-β β β Failed β β
-β βΌ βΌ βΌ β
-β ββββββββββββ ββββββββββββ ββββββββββββ β
-β β expired β βsuspended β ββββββββββββ β past_due β β
-β β β β β Auto β β β
-β ββββββββββββ ββββββββββββ Suspend ββββββββββββ β
-β β β β
-β β Admin β β
-β β Action β β
-β βΌ β β
-β ββββββββββββ β β
-β βcancelled β ββββββββββββββββββββ β
-β β β User Cancel β
-β ββββββββββββ β
-β β
-βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-```
-
-**Each State Should Show:**
-
-| State | Plan Badge | Actions Available | Warnings |
-|-------|------------|-------------------|----------|
-| `trial` | "Trial (X days)" | Upgrade, Buy Credits | "Trial ends [date]" |
-| `pending_payment` | "Awaiting Payment" | Pay Invoice, Change Method | "Complete payment to activate" |
-| `active` | "Active" | Upgrade, Buy Credits, Cancel, Manage | None |
-| `past_due` | "Payment Overdue" | Update Payment, Pay Now | "Update payment to avoid suspension" |
-| `suspended` | "Suspended" | Pay to Reactivate | "Account suspended - pay to restore" |
-| `cancelled` | "Cancels [date]" | Resubscribe | "Access ends [date]" |
-
----
-
-### Payment Options By State (Missing Restrictions)
-
-**Current:** All payment options shown regardless of account state.
-
-**Required Restrictions:**
-
-| Account State | Can Upgrade? | Can Buy Credits? | Can Change Plan? | Can Cancel? |
-|---------------|--------------|------------------|------------------|-------------|
-| `trial` | Yes | Yes | N/A | No |
-| `pending_payment` | No (Pay first) | No (Pay first) | No | No |
-| `active` | Yes | Yes | Yes | Yes |
-| `past_due` | No (Pay first) | No | No | Yes |
-| `suspended` | No | No | No | No |
-| `cancelled` | Yes (Resubscribe) | No | No | No |
-
----
-
-### Summary of PlansAndBillingPage Fixes Needed
-
-| # | Issue | Severity | Effort |
-|---|-------|----------|--------|
-| 1 | Plan shows "Active" with unpaid invoice | CRITICAL | 30 min |
-| 2 | Subscription states not reflected | CRITICAL | 1 hr |
-| 3 | Upgrade available when payment pending | HIGH | 15 min |
-| 4 | Cancel available for unpaid subscriptions | HIGH | 15 min |
-| 5 | Manage Billing shown to non-Stripe users | HIGH | 15 min |
-| 6 | No pending credit purchase indicator | HIGH | 30 min |
-| 7 | Invoice status colors inconsistent | MEDIUM | 15 min |
-| 8 | No payment method shown per invoice | MEDIUM | 30 min |
-| 9 | Renewal date context wrong | MEDIUM | 15 min |
-| 10 | Gateway selection not synced | MEDIUM | 30 min |
-| 11 | Annual billing does nothing | LOW | 2 hrs |
-| 12 | Bank details hardcoded | LOW | 1 hr |
-
----
-
-## Critical Issues (Immediate Action Required)
-
-### 1. PayPal Webhook Signature Not Enforced
-
-**Location:** `paypal_views.py:498-511`
-
-**Problem:**
-```python
-if not is_valid:
- logger.warning("PayPal webhook signature verification failed")
- # Optionally reject invalid signatures
- # return Response({'error': 'Invalid signature'}, status=400) # COMMENTED OUT!
-```
-
-**Risk:** Malicious actors can craft fake webhook events to:
-- Approve payments that never happened
-- Cancel legitimate subscriptions
-- Add credits without payment
-
-**Fix Required:**
-```python
-if not is_valid:
- logger.error("PayPal webhook signature verification failed")
- return Response({'error': 'Invalid signature'}, status=400) # UNCOMMENT
-```
-
----
-
-### 2. Stripe Webhook Not Idempotent (Double-Charge Risk)
-
-**Location:** `stripe_views.py:380-391` (_handle_checkout_completed)
-
-**Problem:** Webhook can be called multiple times for same event. No check prevents duplicate invoice/payment creation.
-
-**Scenario:**
-1. Stripe sends webhook
-2. Invoice and payment created
-3. Stripe retries webhook (network timeout)
-4. **Duplicate invoice and payment created**
-
-**Fix Required:**
-```python
-# At start of _handle_checkout_completed:
-session_id = session.get('id')
-if Payment.objects.filter(stripe_checkout_session_id=session_id).exists():
- logger.info(f"Webhook already processed for session {session_id}")
- return # Already processed
-```
-
----
-
-### 3. PayPal Capture Order Not Idempotent
-
-**Location:** `paypal_views.py:261-365` (PayPalCaptureOrderView)
-
-**Problem:** If frontend calls `/capture-order/` twice (network timeout), payment captured twice.
-
-**Fix Required:**
-```python
-# Check if order already captured
-existing = Payment.objects.filter(paypal_order_id=order_id, status='succeeded').first()
-if existing:
- return Response({'status': 'already_captured', 'payment_id': existing.id})
-```
-
----
-
-### 4. Refund Functions Call Non-Existent Modules
-
-**Location:** `refund_views.py:160-208`
-
-**Problem:**
-```python
-from igny8_core.business.billing.utils.payment_gateways import get_stripe_client # DOESN'T EXIST
-from igny8_core.business.billing.utils.payment_gateways import get_paypal_client # DOESN'T EXIST
-```
-
-**Risk:** Refunds marked as processed but **never actually charged back** to customer.
-
-**Fix Required:** Create the missing modules or use existing service classes:
-```python
-from igny8_core.business.billing.services.stripe_service import StripeService
-from igny8_core.business.billing.services.paypal_service import PayPalService
-```
-
----
-
-### 5. Amount Validation Missing for PayPal
-
-**Location:** `paypal_views.py:569`
-
-**Problem:**
-```python
-amount = float(capture_result.get('amount', package.price)) # Trusts PayPal amount
-```
-
-**Risk:** If PayPal returns wrong amount, system processes it as correct.
-
-**Fix Required:**
-```python
-captured_amount = float(capture_result.get('amount', 0))
-expected_amount = float(package.price)
-if abs(captured_amount - expected_amount) > 0.01: # Allow 1 cent tolerance
- logger.error(f"Amount mismatch: captured={captured_amount}, expected={expected_amount}")
- return Response({'error': 'Amount mismatch'}, status=400)
-```
-
----
-
-## High Priority Issues
-
-### 6. No Admin Dashboard for Pending Payments
-
-**Problem:** Admins must use Django admin to approve manual payments.
-
-**Missing Endpoint:**
-```
-GET /v1/admin/billing/pending-payments/ - List pending approvals
-POST /v1/admin/billing/payments/{id}/approve/ - Approve payment
-POST /v1/admin/billing/payments/{id}/reject/ - Reject payment
-```
-
-**Required Implementation:**
-```python
-class AdminPaymentViewSet(viewsets.ModelViewSet):
- permission_classes = [IsAdminUser]
-
- @action(detail=False, methods=['get'])
- def pending(self, request):
- payments = Payment.objects.filter(status='pending_approval')
- return Response(PaymentSerializer(payments, many=True).data)
-
- @action(detail=True, methods=['post'])
- def approve(self, request, pk=None):
- payment = self.get_object()
- PaymentService.approve_manual_payment(payment, request.user.id, request.data.get('notes'))
- return Response({'status': 'approved'})
-```
-
----
-
-### 7. Invoice Number Race Condition
-
-**Location:** `invoice_service.py:52-78`
-
-**Problem:**
-```python
-count = Invoice.objects.select_for_update().filter(...).count()
-invoice_number = f"{prefix}-{count + 1:04d}"
-while Invoice.objects.filter(invoice_number=invoice_number).exists(): # NOT LOCKED!
- count += 1
-```
-
-**Fix Required:**
-```python
-# Use database unique constraint + retry logic
-@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}"
- try:
- # Use get_or_create with unique constraint
- return invoice_number
- except IntegrityError:
- continue
- raise ValueError("Unable to generate unique invoice number")
-```
-
----
-
-### 8. Browser Redirect Lost After Payment
-
-**Problem:** If user closes browser after Stripe payment but before redirect:
-- Payment succeeds (webhook processes it)
-- User doesn't know payment succeeded
-- May attempt to pay again
-
-**Fix Required:** Add payment status check endpoint:
-```python
-# New endpoint
-GET /v1/billing/payment-status/{session_id}/
-
-# Frontend should check this on /account/plans load
-const checkPaymentStatus = async (sessionId) => {
- const response = await fetch(`/v1/billing/payment-status/${sessionId}/`);
- if (response.data.status === 'completed') {
- toast.success('Your previous payment was successful!');
- refreshUser();
- }
-};
-```
-
----
-
-### 9. Subscription Renewal Gets Stuck
-
-**Location:** `subscription_renewal.py:78-131`
-
-**Problem:** Status set to `pending_renewal` with no expiry or retry mechanism.
-
-**Fix Required:**
-```python
-# Add Celery task
-@app.task
-def check_stuck_renewals():
- """Run daily to check for stuck renewals"""
- stuck = Subscription.objects.filter(
- status='pending_renewal',
- metadata__renewal_required_at__lt=timezone.now() - timedelta(days=7)
- )
- for sub in stuck:
- # Send reminder email
- send_renewal_reminder(sub)
- # After 14 days, suspend
- if sub.metadata.get('renewal_required_at') < timezone.now() - timedelta(days=14):
- sub.status = 'past_due'
- sub.account.status = 'suspended'
- sub.save()
-```
-
----
-
-### 10. Currency Exchange Rates Hardcoded
-
-**Location:** `currency.py:137-157`
-
-**Problem:**
-```python
-CURRENCY_MULTIPLIERS = {
- 'PKR': 278.0, # STATIC - real rate changes daily!
- 'INR': 83.0,
- # ...
-}
-```
-
-**Risk:** Users charged incorrect amounts over time.
-
-**Fix Required:**
-```python
-class ExchangeRateService:
- CACHE_KEY = 'exchange_rates'
- CACHE_TTL = 86400 # 24 hours
-
- @classmethod
- def get_rate(cls, currency):
- rates = cache.get(cls.CACHE_KEY)
- if not rates:
- rates = cls._fetch_from_api()
- cache.set(cls.CACHE_KEY, rates, cls.CACHE_TTL)
- return rates.get(currency, 1.0)
-
- @classmethod
- def _fetch_from_api(cls):
- # Use OpenExchangeRates, Fixer.io, or similar
- response = requests.get('https://api.exchangerate-api.com/v4/latest/USD')
- return response.json()['rates']
-```
-
----
-
-## Medium Priority Issues
-
-### 11. No Promo Code/Discount Support
-
-**Missing Models:**
-```python
-class PromoCode(models.Model):
- code = models.CharField(max_length=50, unique=True)
- discount_type = models.CharField(choices=[('percent', '%'), ('fixed', '$')])
- discount_value = models.DecimalField(max_digits=10, decimal_places=2)
- valid_from = models.DateTimeField()
- valid_until = models.DateTimeField(null=True)
- max_uses = models.IntegerField(null=True)
- current_uses = models.IntegerField(default=0)
- applicable_plans = models.ManyToManyField('Plan', blank=True)
-```
-
----
-
-### 12. No Partial Payment Support
-
-**Current:** User must pay full invoice amount.
-
-**Needed:**
-- Split invoice into multiple payments
-- Track partial payment progress
-- Handle remaining balance
-
----
-
-### 13. No Dunning Management
-
-**Missing:** When payment fails:
-- Day 1: Payment failed notification
-- Day 3: Retry attempt + reminder
-- Day 7: Second retry + warning
-- Day 14: Account suspension warning
-- Day 21: Account suspended
-
----
-
-### 14. No Manual Payment Reference Uniqueness
-
-**Location:** `models.py:487-490`
-
-**Fix:**
-```python
-manual_reference = models.CharField(
- max_length=255,
- blank=True,
- unique=True, # ADD THIS
- null=True # Allow null for non-manual payments
-)
-```
-
----
-
-### 15. Refund Credit Deduction Race Condition
-
-**Location:** `refund_views.py:108-129`
-
-**Fix:** Use `select_for_update()`:
-```python
-with transaction.atomic():
- account = Account.objects.select_for_update().get(id=payment.account_id)
- if account.credit_balance >= credits_to_deduct:
- account.credit_balance -= credits_to_deduct
- account.save()
-```
-
----
-
-### 16. Invoice Total Calculation Silent Failure
-
-**Location:** `models.py:439-448`
-
-**Problem:**
-```python
-except Exception:
- pass # SILENT FAILURE!
-```
-
-**Fix:**
-```python
-except Exception as e:
- logger.error(f"Invalid line item in invoice {self.id}: {item}, error: {e}")
- raise ValueError(f"Invalid invoice line item: {item}")
-```
-
----
-
-### 17. No Webhook Event Storage
-
-**Missing:** All incoming webhooks should be stored for:
-- Audit trail
-- Replay on failure
-- Debugging
-
-**Add Model:**
-```python
-class WebhookEvent(models.Model):
- event_id = models.CharField(max_length=255, unique=True)
- provider = models.CharField(max_length=20) # stripe, paypal
- event_type = models.CharField(max_length=100)
- payload = models.JSONField()
- processed = models.BooleanField(default=False)
- processed_at = models.DateTimeField(null=True)
- error_message = models.TextField(blank=True)
- created_at = models.DateTimeField(auto_now_add=True)
-```
-
----
-
-### 18. No Payment Audit Trail for Rejections
-
-**Missing Fields in Payment model:**
-```python
-rejected_by = models.ForeignKey('User', null=True, related_name='rejected_payments')
-rejected_at = models.DateTimeField(null=True)
-rejection_reason = models.TextField(blank=True)
-```
-
----
-
-## Recommendations Summary
-
-### Immediate (This Week)
-
-| # | Issue | Effort | Impact |
-|---|-------|--------|--------|
-| 1 | Enable PayPal webhook signature verification | 5 min | CRITICAL |
-| 2 | Add Stripe webhook idempotency check | 30 min | CRITICAL |
-| 3 | Add PayPal capture idempotency check | 30 min | CRITICAL |
-| 4 | Fix refund module imports | 1 hr | CRITICAL |
-| 5 | Add PayPal amount validation | 30 min | CRITICAL |
-
-### Short-Term (This Month)
-
-| # | Issue | Effort | Impact |
-|---|-------|--------|--------|
-| 6 | Build admin pending payments dashboard | 1 day | HIGH |
-| 7 | Fix invoice number race condition | 2 hrs | HIGH |
-| 8 | Add payment status check endpoint | 2 hrs | HIGH |
-| 9 | Fix stuck renewal subscriptions | 1 day | HIGH |
-| 10 | Implement dynamic currency rates | 1 day | HIGH |
-
-### Medium-Term (This Quarter)
-
-| # | Issue | Effort | Impact |
-|---|-------|--------|--------|
-| 11 | Promo code system | 3 days | MEDIUM |
-| 12 | Partial payment support | 2 days | MEDIUM |
-| 13 | Dunning management | 2 days | MEDIUM |
-| 14 | Webhook event storage | 1 day | MEDIUM |
-| 15 | Payment audit trail | 1 day | MEDIUM |
-
----
-
-## Architecture Assessment
-
-### What's Good
-
-1. **Clean separation** between frontend entry points, services, and backend
-2. **Country-based logic** correctly isolates Pakistan users from PayPal
-3. **Multi-gateway support** (Stripe, PayPal, Manual) well architected
-4. **Service layer abstraction** (`StripeService`, `PayPalService`, `PaymentService`)
-5. **Invoice and payment tracking** comprehensive
-6. **Webhook handlers** exist for both gateways
-
-### What Needs Improvement
-
-1. **Idempotency** - Critical for payment processing
-2. **Transaction safety** - Need more `@transaction.atomic()` and `select_for_update()`
-3. **Observability** - No webhook event storage, limited metrics
-4. **Admin tooling** - Manual payments need proper dashboard
-5. **Error handling** - Too many silent failures
-6. **Feature gaps** - No promo codes, partial payments, dunning
-
----
-
-## Final Assessment
-
-| Area | Rating | Notes |
-|------|--------|-------|
-| Documentation Accuracy | A | Matches codebase |
-| Security | C | Webhook verification gaps |
-| Reliability | C | Idempotency issues |
-| Completeness | B | Core features present |
-| Admin Experience | D | No proper dashboard |
-| User Experience | B | Good flows, missing status checks |
-| Code Quality | B | Good structure, some silent failures |
-
-**Overall Grade: C+**
-
-The payment system is functional but has critical security and reliability gaps that must be addressed before scaling. The architecture is sound, but implementation details need hardening.
-
----
-
-## Quick Wins (Can Do Today)
-
-1. Uncomment PayPal webhook signature rejection (5 min)
-2. Add `@transaction.atomic()` to all payment handlers (30 min)
-3. Add duplicate check before creating payments (30 min)
-4. Add unique constraint to `manual_reference` (migration)
-5. Remove silent `except: pass` blocks (30 min)
-
----
-
-*Report generated by deep audit of IGNY8 payment system codebase.*
diff --git a/docs/90-REFERENCE/PAYMENT-SYSTEM-REFACTOR-PLAN.md b/docs/90-REFERENCE/PAYMENT-SYSTEM-REFACTOR-PLAN.md
deleted file mode 100644
index cbda4229..00000000
--- a/docs/90-REFERENCE/PAYMENT-SYSTEM-REFACTOR-PLAN.md
+++ /dev/null
@@ -1,2365 +0,0 @@
-# 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`
-
-```python
-# 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`
-
-```python
-# 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`
-
-```python
-# 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`
-
-```python
-# 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`
-
-```python
-# 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):**
-
-```python
-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):**
-
-```python
-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)
-
-```python
-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`
-
-```python
-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)
-
-```python
-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
-
-```python
-# In Invoice model
-invoice_number = models.CharField(max_length=50, unique=True) # Already has unique=True
-```
-
-**Update invoice_service.py** to handle IntegrityError:
-
-```python
-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:**
-
-```python
-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:**
-
-```tsx
-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(null);
- const [countries, setCountries] = useState([]);
- 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 (
-
- );
-}
-```
-
----
-
-### 2.2 Remove /signup/pk Route β
-
-**File:** `frontend/src/routes/index.tsx` (or equivalent)
-
-**REMOVE:** β
-```tsx
-// DELETE THIS ROUTE
- } />
-```
-
-**KEEP only:** β
-```tsx
- } />
-```
-
----
-
-### 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: β
-
-```tsx
-// 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:** β
-
-```tsx
-export default function PlansAndBillingPage() {
- const { user, refreshUser } = useAuthStore();
- const [loading, setLoading] = useState(true);
- const [invoices, setInvoices] = useState([]);
- const [payments, setPayments] = useState([]);
-
- 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 ;
-
- // NEW USER - Show full-page payment view
- if (isNewUserPendingPayment) {
- return (
- i.status === 'pending')}
- userCountry={billingCountry}
- onPaymentSuccess={() => {
- refreshUser();
- loadBillingData();
- }}
- />
- );
- }
-
- // EXISTING USER - Show normal billing dashboard
- return (
- i.status === 'pending')}
- onRefresh={loadBillingData}
- />
- );
-}
-```
-
----
-
-### 2.5 Create PendingPaymentView Component β
-
-**File:** `frontend/src/components/billing/PendingPaymentView.tsx` (NEW) β
-
-```tsx
-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(availableMethods[0]);
- const [loading, setLoading] = useState(false);
- const [bankDetails, setBankDetails] = useState(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 No pending invoice found
;
- }
-
- return (
-
- {/* Header */}
-
-
- Complete Your Subscription
-
-
- Your {invoice.subscription?.plan?.name} plan is ready to activate
-
-
-
- {/* Invoice Summary Card */}
-
-
-
-
Invoice
-
#{invoice.invoice_number}
-
-
-
Amount Due
-
- {invoice.currency} {invoice.total_amount}
-
-
-
-
-
- Plan
- {invoice.subscription?.plan?.name}
-
-
- Billing Period
- Monthly
-
-
-
-
- {/* Payment Method Selection */}
-
- Select Payment Method
-
- {availableMethods.map((method) => (
- 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' && }
- {method === 'paypal' && }
- {method === 'bank_transfer' && }
-
- {method === 'stripe' && 'Credit/Debit Card'}
- {method === 'paypal' && 'PayPal'}
- {method === 'bank_transfer' && 'Bank Transfer'}
-
-
- ))}
-
-
-
- {/* Payment Form */}
-
- {selectedMethod === 'stripe' && (
-
-
-
- Pay securely with your credit or debit card via Stripe
-
-
- Pay {invoice.currency} {invoice.total_amount}
-
-
- )}
-
- {selectedMethod === 'paypal' && (
-
-
-
- Pay securely with your PayPal account
-
-
- Pay with PayPal
-
-
- )}
-
- {selectedMethod === 'bank_transfer' && (
-
- )}
-
-
- {/* Change Plan Link */}
-
- setShowPlanSelector(true)}
- className="text-sm text-gray-500 hover:text-gray-700"
- >
- Need a different plan?
-
-
-
- );
-}
-```
-
----
-
-### 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
-
-```tsx
-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 (
-
- {/* Pending Payment Banner for existing users */}
- {hasPendingInvoice && (
-
-
-
-
Payment Required
-
Complete your payment to continue using all features
-
-
setShowPaymentModal(true)}
- >
- Pay Now
-
-
-
- )}
-
- {/* Current Plan Card */}
-
-
-
-
{user?.account?.plan?.name}
-
- {statusDisplay.label}
-
-
-
- {canManageBilling && (
-
- Manage Billing
-
- )}
- {canUpgrade && (
- setShowUpgradeModal(true)}>
- Upgrade
-
- )}
-
-
-
-
- {/* Credits Card */}
-
-
-
-
Credit Balance
-
{user?.account?.credits?.toLocaleString()}
-
- {canBuyCredits && (
-
setShowCreditsModal(true)}>
- Buy Credits
-
- )}
-
-
-
- {/* Billing History */}
-
- Billing History
-
-
-
- {/* Cancel Subscription - only for active paid users */}
- {canCancel && subscription && (
-
- setShowCancelModal(true)}
- className="text-sm text-gray-500 hover:text-error-500"
- >
- Cancel Subscription
-
-
- )}
-
- {/* Modals */}
-
setShowPaymentModal(false)}
- invoice={invoices.find(i => i.status === 'pending')}
- userCountry={user?.account?.billing_country}
- onSuccess={onRefresh}
- />
-
- setShowCreditsModal(false)}
- userCountry={user?.account?.billing_country}
- onSuccess={onRefresh}
- />
-
- );
-}
-```
-
----
-
-### 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: β
-
-```tsx
-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 ;
- }
-
- return (
-
- {/* Bank Details - FROM BACKEND */}
-
-
Bank Transfer Details
-
-
Bank: {bankDetails.bank_name}
-
Account Title: {bankDetails.account_title}
-
Account #: {bankDetails.account_number}
-
IBAN: {bankDetails.iban}
-
Reference: {invoiceNumber}
-
Amount: {amount}
-
-
-
- {/* Form fields */}
- {/* ... */}
-
- );
-}
-```
-
----
-
-### 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:** β
-
-```tsx
-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:** β
-```tsx
-
-```
-**Done:** Route now redirects to main SignUp component
-
----
-
-## Phase 4: Database Migrations β
-
-### 4.1 Create Migration for WebhookEvent Model β
-
-```bash
-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 β
-
-```bash
-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
-
-```bash
-# 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
-
-```bash
-# 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:**
-```tsx
-// 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:**
-```tsx
-// 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:**
-```tsx
-// 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:**
-```python
-# 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:**
-```python
-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 `token` β `order_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: ,
- 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: ,
- 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:**
-```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:**
-```python
-# 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:**
-```tsx
-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:**
-```tsx
-// 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:**
-```tsx
-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:**
-```tsx
-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):
-5. Dynamic currency display
-6. Fix hasActivePlan logic
-7. Fix Manage Billing button logic
-8. Add webhook event storage
-
-### MEDIUM PRIORITY (Do After):
-9. Real-time status updates (polling/websocket)
-10. Email notifications
-11. 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.*
diff --git a/docs/90-REFERENCE/PAYMENT-SYSTEM.md b/docs/90-REFERENCE/PAYMENT-SYSTEM.md
new file mode 100644
index 00000000..2aaa79c7
--- /dev/null
+++ b/docs/90-REFERENCE/PAYMENT-SYSTEM.md
@@ -0,0 +1,677 @@
+# Payment System Documentation
+
+> **Version:** 1.6.0
+> **Last Updated:** January 8, 2026
+> **Status:** Production Ready
+
+This document provides comprehensive documentation of the IGNY8 payment system architecture, implementation, and flows.
+
+---
+
+## Table of Contents
+
+1. [System Overview](#system-overview)
+2. [Payment Entry Points](#payment-entry-points)
+3. [Backend Architecture](#backend-architecture)
+4. [Frontend Architecture](#frontend-architecture)
+5. [Payment Flows](#payment-flows)
+6. [Country-Based Payment Rules](#country-based-payment-rules)
+7. [Webhook Processing](#webhook-processing)
+8. [Models Reference](#models-reference)
+9. [Security Features](#security-features)
+
+---
+
+## System Overview
+
+### Supported Payment Methods
+
+| Method | Type | Regions | Use Cases |
+|--------|------|---------|-----------|
+| **Stripe** | Credit/Debit Card | Global | Subscriptions, Credit packages |
+| **PayPal** | PayPal account | Global (except PK) | Subscriptions, Credit packages |
+| **Bank Transfer** | Manual | Pakistan (PK) | Subscriptions, Credit packages |
+
+### Payment Method Selection Logic
+
+```
+ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β Country-Based Payment Rules β
+ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
+β β
+β Global Users (non-PK): β
+β β
Stripe (Credit/Debit Card) β
+β β
PayPal β
+β β Bank Transfer (not available) β
+β β
+β Pakistan Users (PK): β
+β β
Stripe (Credit/Debit Card) β
+β β PayPal (not available in PK) β
+β β
Bank Transfer (manual) β
+β β
+ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+```
+
+### Architecture Overview
+
+```
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β PAYMENT SYSTEM FLOW β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
+β β
+β ββββββββββββββββ ββββββββββββββββ β
+β β Signup βββββββββΆ β /account/ β β
+β β (no pay) β β plans β β
+β ββββββββββββββββ ββββββββ¬ββββββββ β
+β β β
+β βββββββββββββββ΄ββββββββββββββ β
+β β β β
+β New User? Existing User? β
+β β β β
+β βΌ βΌ β
+β ββββββββββββββββ ββββββββββββββββ β
+β β PendingPay- β β Plans/Billingβ β
+β β mentView β β Dashboard β β
+β ββββββββ¬ββββββββ ββββββββ¬ββββββββ β
+β β β β
+β βββββββββββΌββββββββββ ββββββββββΌβββββββββ β
+β β β β β β β β
+β βΌ βΌ βΌ βΌ βΌ βΌ β
+β Stripe PayPal Bank Upgrade Credits Manage β
+β Transfer β
+β β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+```
+
+---
+
+## Payment Entry Points
+
+### 1. Signup Flow
+
+**File:** `frontend/src/components/auth/SignUpFormUnified.tsx`
+
+**Simplified Signup (No Payment on Signup):**
+- User selects plan and provides details
+- Account created with `status='pending_payment'` for paid plans
+- User redirected to `/account/plans` to complete payment
+- No payment gateway redirect from signup page
+
+```typescript
+// Signup flow creates account only, no checkout
+const handleSignup = async (data) => {
+ const result = await register({
+ email, password, plan_slug, billing_country
+ });
+ // Redirect to plans page for payment
+ navigate('/account/plans');
+};
+```
+
+### 2. Plans & Billing Page
+
+**File:** `frontend/src/pages/account/PlansAndBillingPage.tsx`
+
+Central hub for all payment-related actions:
+
+**For New Users (pending_payment):**
+- Shows `PendingPaymentView` component
+- Full-page payment interface
+- Invoice details and payment method selection
+
+**For Existing Users:**
+- Current plan and subscription status
+- Credit balance and purchase
+- Invoice history and downloads
+- Subscription management
+
+### 3. PendingPaymentView Component
+
+**File:** `frontend/src/components/billing/PendingPaymentView.tsx`
+
+Full-page payment interface for new users:
+- Displays invoice details and plan info
+- Payment method selection based on country
+- Stripe/PayPal redirect or Bank Transfer form
+- Status checking for bank transfer submissions
+
+### 4. Bank Transfer Form
+
+**File:** `frontend/src/components/billing/BankTransferForm.tsx`
+
+Manual payment submission for Pakistan users:
+- Bank account details display
+- Transaction reference input
+- File upload for payment proof
+- Submission and status tracking
+
+---
+
+## Backend Architecture
+
+### Service Layer
+
+#### StripeService
+
+**File:** `backend/igny8_core/business/billing/services/stripe_service.py`
+
+| Method | Description |
+|--------|-------------|
+| `create_checkout_session()` | Create subscription checkout |
+| `create_credit_checkout_session()` | Create credit package checkout |
+| `create_billing_portal_session()` | Customer billing portal |
+| `get_or_create_customer()` | Stripe customer management |
+| `construct_webhook_event()` | Verify webhook signatures |
+
+#### PayPalService
+
+**File:** `backend/igny8_core/business/billing/services/paypal_service.py`
+
+| Method | Description |
+|--------|-------------|
+| `create_order()` | Create one-time payment order |
+| `create_subscription_order()` | Create subscription order |
+| `capture_order()` | Capture approved payment |
+| `verify_webhook_signature()` | Webhook verification |
+
+#### InvoiceService
+
+**File:** `backend/igny8_core/business/billing/services/invoice_service.py`
+
+| Method | Description |
+|--------|-------------|
+| `create_subscription_invoice()` | Invoice for plan subscription |
+| `create_credit_package_invoice()` | Invoice for credit purchase |
+| `mark_paid()` | Mark invoice as paid |
+| `generate_pdf()` | Generate PDF invoice |
+
+#### PaymentService
+
+**File:** `backend/igny8_core/business/billing/services/payment_service.py`
+
+| Method | Description |
+|--------|-------------|
+| `create_stripe_payment()` | Record Stripe payment |
+| `create_paypal_payment()` | Record PayPal payment |
+| `create_manual_payment()` | Record bank transfer |
+| `approve_manual_payment()` | Admin approval |
+
+### API Endpoints
+
+#### Stripe Endpoints
+
+| Endpoint | Method | Description |
+|----------|--------|-------------|
+| `/v1/billing/stripe/config/` | GET | Publishable key |
+| `/v1/billing/stripe/checkout/` | POST | Create checkout session |
+| `/v1/billing/stripe/credit-checkout/` | POST | Credit package checkout |
+| `/v1/billing/stripe/billing-portal/` | POST | Billing portal |
+| `/v1/billing/webhooks/stripe/` | POST | Webhook handler |
+
+#### PayPal Endpoints
+
+| Endpoint | Method | Description |
+|----------|--------|-------------|
+| `/v1/billing/paypal/config/` | GET | Client ID |
+| `/v1/billing/paypal/create-order/` | POST | Credit package order |
+| `/v1/billing/paypal/create-subscription-order/` | POST | Subscription order |
+| `/v1/billing/paypal/capture-order/` | POST | Capture payment |
+| `/v1/billing/webhooks/paypal/` | POST | Webhook handler |
+
+#### Invoice Endpoints
+
+| Endpoint | Method | Description |
+|----------|--------|-------------|
+| `/v1/billing/invoices/` | GET | List invoices |
+| `/v1/billing/invoices/{id}/` | GET | Invoice detail |
+| `/v1/billing/invoices/{id}/download_pdf/` | GET | Download PDF |
+
+#### Payment Endpoints
+
+| Endpoint | Method | Description |
+|----------|--------|-------------|
+| `/v1/billing/payments/` | GET | List payments |
+| `/v1/billing/payments/manual/` | POST | Submit bank transfer |
+| `/v1/billing/admin/payments/confirm/` | POST | Admin approve/reject |
+
+---
+
+## Frontend Architecture
+
+### Services
+
+**File:** `frontend/src/services/billing.api.ts`
+
+Key functions:
+
+```typescript
+// Gateway availability (country-based)
+getAvailablePaymentGateways(userCountry?: string)
+
+// Subscription helpers
+subscribeToPlan(planId, gateway, options)
+purchaseCredits(packageId, gateway, options)
+
+// Stripe functions
+createStripeCheckout(planId, options)
+createStripeCreditCheckout(packageId, options)
+openStripeBillingPortal(returnUrl)
+
+// PayPal functions
+createPayPalSubscriptionOrder(planId, options)
+createPayPalCreditOrder(packageId, options)
+capturePayPalOrder(orderId, metadata)
+
+// Manual payment
+submitManualPayment(invoiceId, data)
+```
+
+### Components
+
+| Component | File | Purpose |
+|-----------|------|---------|
+| `PendingPaymentView` | `/components/billing/PendingPaymentView.tsx` | New user payment |
+| `BankTransferForm` | `/components/billing/BankTransferForm.tsx` | Bank transfer submission |
+| `PendingPaymentBanner` | `/components/billing/PendingPaymentBanner.tsx` | Alert for pending payments |
+| `PaymentGatewaySelector` | `/components/billing/PaymentGatewaySelector.tsx` | Gateway selection UI |
+| `PayInvoiceModal` | `/components/billing/PayInvoiceModal.tsx` | Pay invoice modal |
+
+---
+
+## Payment Flows
+
+### Flow 1: New User Signup with Stripe
+
+```
+1. User submits signup form with plan
+2. Backend creates:
+ - User account
+ - Account (status='pending_payment')
+ - Subscription (status='pending_payment')
+ - Invoice (status='pending')
+3. User redirected to /account/plans
+4. PendingPaymentView displays
+5. User selects Stripe, clicks Pay
+6. Redirect to Stripe Checkout
+7. User completes payment
+8. Stripe webhook received:
+ - Payment recorded
+ - Invoice marked paid
+ - Account activated
+ - Credits added
+9. User redirected back to /account/plans
+10. Success message, dashboard displays
+```
+
+### Flow 2: New User with PayPal (Non-PK)
+
+```
+1. Same as Stripe steps 1-4
+5. User selects PayPal, clicks Pay
+6. PayPal order created
+7. Redirect to PayPal approval
+8. User approves on PayPal
+9. Redirect back with order_id
+10. Frontend calls capture-order
+11. Backend processes:
+ - Payment captured
+ - Payment recorded
+ - Invoice marked paid
+ - Account activated
+ - Credits added
+12. Success displayed
+```
+
+### Flow 3: Pakistan User with Bank Transfer
+
+```
+1. Same as signup steps 1-4
+5. User sees Stripe + Bank Transfer options
+6. User selects Bank Transfer
+7. BankTransferForm displays:
+ - Bank details (SCB Pakistan)
+ - Reference input
+ - Proof upload option
+8. User makes transfer, submits form
+9. Backend creates:
+ - Payment (status='pending_approval')
+10. User sees "Awaiting Approval" status
+11. Admin reviews in Django Admin
+12. Admin approves:
+ - Payment marked succeeded
+ - Invoice marked paid
+ - Account activated
+ - Credits added
+13. User receives email confirmation
+```
+
+### Flow 4: Existing User Buys Credits
+
+```
+1. User on /account/plans clicks "Buy Credits"
+2. Credit package selection modal
+3. User selects package and gateway
+4. For Stripe/PayPal: redirect flow
+5. For Bank Transfer: form submission
+6. On success: credits added to account
+```
+
+---
+
+## Country-Based Payment Rules
+
+### Implementation in Frontend
+
+```typescript
+// billing.api.ts
+export async function getAvailablePaymentGateways(userCountry?: string) {
+ const [stripeAvailable, paypalAvailable] = await Promise.all([
+ isStripeConfigured(),
+ isPayPalConfigured(),
+ ]);
+
+ const isPakistan = userCountry?.toUpperCase() === 'PK';
+
+ return {
+ stripe: stripeAvailable,
+ // PayPal: NOT available for Pakistan
+ paypal: !isPakistan && paypalAvailable,
+ // Bank Transfer: ONLY for Pakistan
+ manual: isPakistan,
+ };
+}
+```
+
+### Usage in Components
+
+```typescript
+// PendingPaymentView.tsx
+const isPakistan = userCountry === 'PK';
+
+// Load gateways with country filter
+const gateways = await getAvailablePaymentGateways(userCountry);
+
+// Show appropriate options
+const paymentOptions = [
+ { type: 'stripe', ... }, // Always shown if configured
+ isPakistan ? { type: 'manual', ... } : { type: 'paypal', ... },
+];
+```
+
+---
+
+## Webhook Processing
+
+### Stripe Webhooks
+
+**Endpoint:** `POST /v1/billing/webhooks/stripe/`
+
+| Event | Handler Action |
+|-------|----------------|
+| `checkout.session.completed` | Activate subscription, add credits |
+| `invoice.paid` | Add renewal credits |
+| `invoice.payment_failed` | Send notification |
+| `customer.subscription.updated` | Sync changes |
+| `customer.subscription.deleted` | Cancel subscription |
+
+**Idempotency:** Checks `WebhookEvent` model before processing:
+```python
+# Check if already processed
+if WebhookEvent.objects.filter(
+ provider='stripe',
+ event_id=event_id,
+ status='processed'
+).exists():
+ return # Already handled
+```
+
+### PayPal Webhooks
+
+**Endpoint:** `POST /v1/billing/webhooks/paypal/`
+
+| Event | Handler Action |
+|-------|----------------|
+| `CHECKOUT.ORDER.APPROVED` | Auto-capture if configured |
+| `PAYMENT.CAPTURE.COMPLETED` | Mark succeeded, add credits |
+| `PAYMENT.CAPTURE.DENIED` | Mark failed |
+| `BILLING.SUBSCRIPTION.ACTIVATED` | Activate subscription |
+
+**Signature Verification:** Enabled and enforced:
+```python
+is_valid = service.verify_webhook_signature(...)
+if not is_valid:
+ return Response({'error': 'Invalid signature'}, status=400)
+```
+
+---
+
+## Models Reference
+
+### Invoice Model
+
+```python
+class Invoice(AccountBaseModel):
+ STATUS_CHOICES = [
+ ('draft', 'Draft'),
+ ('pending', 'Pending'),
+ ('paid', 'Paid'),
+ ('void', 'Void'),
+ ('uncollectible', 'Uncollectible'),
+ ]
+
+ invoice_number = models.CharField(max_length=50, unique=True)
+ subscription = models.ForeignKey('auth.Subscription', ...)
+ status = models.CharField(max_length=20, choices=STATUS_CHOICES)
+ subtotal = models.DecimalField(...)
+ tax = models.DecimalField(...)
+ total = models.DecimalField(...)
+ currency = models.CharField(max_length=3, default='USD')
+ due_date = models.DateField()
+ line_items = models.JSONField(default=list)
+```
+
+### Payment Model
+
+```python
+class Payment(AccountBaseModel):
+ STATUS_CHOICES = [
+ ('pending', 'Pending'),
+ ('pending_approval', 'Pending Approval'),
+ ('succeeded', 'Succeeded'),
+ ('failed', 'Failed'),
+ ('refunded', 'Refunded'),
+ ]
+
+ PAYMENT_METHOD_CHOICES = [
+ ('stripe', 'Stripe'),
+ ('paypal', 'PayPal'),
+ ('bank_transfer', 'Bank Transfer'),
+ ('manual', 'Manual'),
+ ]
+
+ invoice = models.ForeignKey('Invoice', ...)
+ amount = models.DecimalField(...)
+ currency = models.CharField(max_length=3)
+ payment_method = models.CharField(choices=PAYMENT_METHOD_CHOICES)
+ status = models.CharField(choices=STATUS_CHOICES)
+ stripe_payment_intent_id = models.CharField(...)
+ paypal_order_id = models.CharField(...)
+ manual_reference = models.CharField(..., unique=True)
+```
+
+### WebhookEvent Model
+
+```python
+class WebhookEvent(models.Model):
+ """Audit trail for webhook processing"""
+ PROVIDER_CHOICES = [
+ ('stripe', 'Stripe'),
+ ('paypal', 'PayPal'),
+ ]
+
+ provider = models.CharField(choices=PROVIDER_CHOICES)
+ event_id = models.CharField(max_length=255)
+ event_type = models.CharField(max_length=100)
+ payload = models.JSONField()
+ status = models.CharField() # 'pending', 'processed', 'failed'
+ processed_at = models.DateTimeField(null=True)
+ error_message = models.TextField(blank=True)
+```
+
+---
+
+## Security Features
+
+### Implemented Security Measures
+
+1. **Webhook Signature Verification**
+ - Stripe: `stripe.Webhook.construct_event()` with signing secret
+ - PayPal: `verify_webhook_signature()` API call
+
+2. **Idempotency**
+ - `WebhookEvent` model tracks processed events
+ - Duplicate detection before processing
+
+3. **Amount Validation**
+ - PayPal capture validates amount matches expected
+ - Prevents manipulation attacks
+
+4. **Manual Reference Uniqueness**
+ - Database constraint prevents duplicate bank transfer references
+ - Prevents double submission
+
+5. **CSRF Protection**
+ - Webhook endpoints exempt (external callers)
+ - All other endpoints protected
+
+6. **Authentication**
+ - Payment endpoints require `IsAuthenticatedAndActive`
+ - Config endpoints allow `AllowAny` (public keys only)
+
+---
+
+## Admin Operations
+
+### Django Admin Features
+
+**Location:** Django Admin > Billing
+
+- **Invoices:** View, filter, download PDF
+- **Payments:** View, approve/reject manual payments
+- **Credit Transactions:** Audit trail
+- **Credit Packages:** Manage packages
+
+### Manual Payment Approval
+
+```
+1. Admin navigates to Payments in Django Admin
+2. Filter by status='pending_approval'
+3. Review payment details and proof
+4. Click "Approve" or "Reject" action
+5. System automatically:
+ - Updates payment status
+ - Marks invoice paid (if approved)
+ - Activates account (if approved)
+ - Adds credits (if approved)
+ - Sends email notification
+```
+
+---
+
+## Configuration
+
+### Environment Variables
+
+```bash
+# Stripe
+STRIPE_SECRET_KEY=sk_...
+STRIPE_PUBLISHABLE_KEY=pk_...
+STRIPE_WEBHOOK_SECRET=whsec_...
+
+# PayPal
+PAYPAL_CLIENT_ID=...
+PAYPAL_CLIENT_SECRET=...
+PAYPAL_WEBHOOK_ID=...
+PAYPAL_MODE=sandbox|live
+
+# General
+DEFAULT_CURRENCY=USD
+```
+
+### IntegrationProvider Setup
+
+Payment gateways configured via `IntegrationProvider` model in Django Admin:
+
+1. **Stripe Provider:**
+ - Name: "Stripe"
+ - Provider Type: "stripe"
+ - Credentials: `{"secret_key": "...", "publishable_key": "...", "webhook_secret": "..."}`
+
+2. **PayPal Provider:**
+ - Name: "PayPal"
+ - Provider Type: "paypal"
+ - Credentials: `{"client_id": "...", "client_secret": "...", "webhook_id": "..."}`
+
+---
+
+## Troubleshooting
+
+### Common Issues
+
+| Issue | Cause | Solution |
+|-------|-------|----------|
+| "Stripe not configured" | Missing IntegrationProvider | Add Stripe provider in admin |
+| "PayPal not configured" | Missing IntegrationProvider | Add PayPal provider in admin |
+| PayPal shown for PK users | Country not passed correctly | Ensure `billing_country` saved on account |
+| Duplicate payments | Webhook retry without idempotency | Check `WebhookEvent` for duplicates |
+| PDF download fails | Missing `reportlab` | Run `pip install reportlab` |
+
+### Debug Logging
+
+Enable billing debug logs:
+```python
+# settings.py
+LOGGING = {
+ 'loggers': {
+ 'igny8_core.business.billing': {
+ 'level': 'DEBUG',
+ },
+ },
+}
+```
+
+---
+
+## File Reference
+
+### Backend Files
+
+| File | Description |
+|------|-------------|
+| `billing/views/stripe_views.py` | Stripe API endpoints |
+| `billing/views/paypal_views.py` | PayPal API endpoints |
+| `billing/views/refund_views.py` | Refund processing |
+| `billing/services/stripe_service.py` | Stripe service layer |
+| `billing/services/paypal_service.py` | PayPal service layer |
+| `billing/services/invoice_service.py` | Invoice operations |
+| `billing/services/payment_service.py` | Payment operations |
+| `billing/services/pdf_service.py` | PDF generation |
+| `billing/services/email_service.py` | Email notifications |
+| `billing/models.py` | Billing models |
+| `billing/urls.py` | URL routing |
+
+### Frontend Files
+
+| File | Description |
+|------|-------------|
+| `services/billing.api.ts` | API client functions |
+| `pages/account/PlansAndBillingPage.tsx` | Main billing page |
+| `components/billing/PendingPaymentView.tsx` | New user payment |
+| `components/billing/BankTransferForm.tsx` | Bank transfer form |
+| `components/billing/PayInvoiceModal.tsx` | Invoice payment modal |
+| `components/billing/PaymentGatewaySelector.tsx` | Gateway selection |
+| `components/billing/PendingPaymentBanner.tsx` | Payment alert banner |
+
+---
+
+*Document generated from production codebase - January 8, 2026*
diff --git a/docs/INDEX.md b/docs/INDEX.md
index 9ad9211f..d5f18fc0 100644
--- a/docs/INDEX.md
+++ b/docs/INDEX.md
@@ -1,7 +1,7 @@
# IGNY8 Technical Documentation
-**Version:** 1.4.0
-**Last Updated:** January 6, 2026
+**Version:** 1.6.0
+**Last Updated:** January 8, 2026
**Purpose:** Complete technical reference for the IGNY8 AI content platform
---
@@ -20,6 +20,7 @@
| Understand frontend structure | [30-FRONTEND/PAGES.md](30-FRONTEND/PAGES.md) |
| Trace a workflow end-to-end | [40-WORKFLOWS/](#workflows) |
| Look up model fields | [90-REFERENCE/MODELS.md](90-REFERENCE/MODELS.md) |
+| **Payment system (Stripe/PayPal/Bank)** | [90-REFERENCE/PAYMENT-SYSTEM.md](90-REFERENCE/PAYMENT-SYSTEM.md) |
| See prelaunch checklist | [plans/FINAL-PRELAUNCH.md](plans/FINAL-PRELAUNCH.md) |
| **Understand publishing flow** | [50-DEPLOYMENT/WORDPRESS-INTEGRATION-FLOW.md](50-DEPLOYMENT/WORDPRESS-INTEGRATION-FLOW.md) |
| **AI model architecture (v1.4.0)** | [plans/4th-jan-refactor/final-model-schemas.md](plans/4th-jan-refactor/final-model-schemas.md) |
@@ -138,6 +139,7 @@
|----------|---------|
| [MODELS.md](90-REFERENCE/MODELS.md) | All database models and fields |
| [AI-FUNCTIONS.md](90-REFERENCE/AI-FUNCTIONS.md) | AI engine capabilities and costs |
+| [PAYMENT-SYSTEM.md](90-REFERENCE/PAYMENT-SYSTEM.md) | Payment gateways (Stripe, PayPal, Bank Transfer) |
| [TROUBLESHOOTING.md](90-REFERENCE/TROUBLESHOOTING.md) | Common issues and fixes |
---