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

1121 lines
38 KiB
Markdown

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