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