1121 lines
38 KiB
Markdown
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
|
|
```
|