Files
igny8/docs/90-REFERENCE/PAYMENT-SYSTEM-ARCHITECTURE.md
2026-01-07 13:02:53 +00:00

38 KiB

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
  2. Payment Entry Points (Frontend)
  3. Frontend Payment Services
  4. Backend Endpoints
  5. Backend Services
  6. Models
  7. Payment Method Configuration
  8. Payment Flows
  9. 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

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:

{
  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

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):

// 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):

// 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):

// 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

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):

const isPakistan = userCountry?.toUpperCase() === 'PK';

// Available options based on country:
const availableOptions: PaymentOption[] = isPakistan 
  ? ['stripe', 'bank_transfer'] 
  : ['stripe', 'paypal'];

Stripe Payment (Lines 115-140):

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):

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

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:

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

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):

// 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

Total Lines: 534

Register Function (Lines 230-395):

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

# 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

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

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 (Lines 40-170)

Also: backend/igny8_core/auth/serializers.py (Lines 280-500)

RegisterSerializer.create() Flow (Lines 340-500):

  1. Plan Lookup (Lines 340-365):

    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):

    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):

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

# 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

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:

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

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:

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

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:

# 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

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:

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 (Lines 55-145)

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 (Lines 395-440)

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 (Lines 300-395)

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 (Lines 310-380)

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 (Lines 460-540)

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 (Lines 700-750)

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 (Lines 600-695)

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

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

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

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)

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):

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