38 KiB
Complete Payment System Architecture
Version: 1.0
Last Updated: January 7, 2026
Status: Complete Documentation
This document provides comprehensive documentation of the IGNY8 payment system architecture, including all entry points, services, models, and flows.
Table of Contents
- System Overview
- Payment Entry Points (Frontend)
- Frontend Payment Services
- Backend Endpoints
- Backend Services
- Models
- Payment Method Configuration
- Payment Flows
- Webhooks
System Overview
Supported Payment Methods
| Method | Type | Regions | Use Cases |
|---|---|---|---|
| Stripe | Credit/Debit Card | Global | Subscriptions, Credit packages |
| PayPal | PayPal account | Global (except PK) | Subscriptions, Credit packages |
| Bank Transfer | Manual | Pakistan (PK) | Subscriptions, Credit packages |
| Local Wallet | Manual | Configurable | Credit packages |
Payment Method Selection Logic
- Global Users: Stripe (Card) + PayPal
- Pakistan Users: Stripe (Card) + Bank Transfer (NO PayPal)
Payment Entry Points (Frontend)
1. SignUpFormUnified.tsx
File: frontend/src/components/auth/SignUpFormUnified.tsx
Purpose: Handles payment method selection and checkout redirect during user registration.
Key Features (Lines 1-770)
- Payment Method Loading: Lines 105-195
- Form Submission with Payment: Lines 213-344
- Payment Gateway Redirect Logic: Lines 315-340
Payment Flow:
- Loads available payment methods from
/v1/billing/payment-configs/payment-methods/(Lines 110-195) - Determines available options based on country:
- Pakistan (
/signup/pk): Stripe + Bank Transfer - Global (
/signup): Stripe + PayPal
- Pakistan (
- On form submit (Lines 213-344):
- Calls
register()from authStore - Backend returns
checkout_urlfor Stripe/PayPal - Redirects to payment gateway OR plans page (bank transfer)
- Calls
Data Passed to Backend:
{
email, password, username, first_name, last_name,
account_name,
plan_slug: selectedPlan.slug,
payment_method: selectedPaymentMethod, // 'stripe' | 'paypal' | 'bank_transfer'
billing_email,
billing_country
}
2. PlansAndBillingPage.tsx
File: frontend/src/pages/account/PlansAndBillingPage.tsx
Purpose: Comprehensive billing dashboard for subscription management, credit purchases, and payment history.
Key Features (Lines 1-1197)
- Payment Gateway Return Handler: Lines 100-155
- Plan Selection (handleSelectPlan): Lines 283-310
- Credit Purchase (handlePurchaseCredits): Lines 330-352
- Billing Portal (handleManageSubscription): Lines 354-361
- Invoice Download: Lines 363-376
Payment Flows:
1. Plan Upgrade (handleSelectPlan - Lines 283-310):
// For Stripe/PayPal:
const { redirect_url } = await subscribeToPlan(planId.toString(), selectedGateway);
window.location.href = redirect_url;
// For Manual/Bank Transfer:
await createSubscription({ plan_id: planId, payment_method: selectedPaymentMethod });
2. Credit Purchase (handlePurchaseCredits - Lines 330-352):
// For Stripe/PayPal:
const { redirect_url } = await purchaseCredits(packageId.toString(), selectedGateway);
window.location.href = redirect_url;
// For Manual/Bank Transfer:
await purchaseCreditPackage({ package_id: packageId, payment_method: selectedPaymentMethod });
3. Return URL Handling (Lines 100-155):
// PayPal Success - capture order
if (paypalStatus === 'success' && paypalToken) {
await capturePayPalOrder(paypalToken, { plan_id, package_id });
}
// Stripe Success
if (success === 'true') {
toast.success('Subscription activated!');
await refreshUser();
}
3. PayInvoiceModal.tsx
File: frontend/src/components/billing/PayInvoiceModal.tsx
Purpose: Modal for paying pending invoices with multiple payment methods.
Key Features (Lines 1-544)
- Payment Method Selection: Lines 55-95
- Stripe Payment Handler: Lines 115-140
- PayPal Payment Handler: Lines 142-167
- Bank Transfer Form: Lines 203-250
Payment Method Logic (Lines 55-95):
const isPakistan = userCountry?.toUpperCase() === 'PK';
// Available options based on country:
const availableOptions: PaymentOption[] = isPakistan
? ['stripe', 'bank_transfer']
: ['stripe', 'paypal'];
Stripe Payment (Lines 115-140):
const handleStripePayment = async () => {
const result = await subscribeToPlan(planIdentifier, 'stripe', {
return_url: `${window.location.origin}/account/plans?success=true`,
cancel_url: `${window.location.origin}/account/plans?canceled=true`,
});
window.location.href = result.redirect_url;
};
Bank Transfer Submission (Lines 203-250):
const handleBankSubmit = async () => {
await fetch(`${API_BASE_URL}/v1/billing/admin/payments/confirm/`, {
method: 'POST',
body: JSON.stringify({
invoice_id: invoice.id,
payment_method: 'bank_transfer',
amount: invoice.total_amount,
manual_reference: bankFormData.manual_reference,
manual_notes: bankFormData.manual_notes,
proof_url: bankFormData.proof_url,
}),
});
};
4. PendingPaymentBanner.tsx
File: frontend/src/components/billing/PendingPaymentBanner.tsx
Purpose: Alert banner displayed when account status is pending_payment.
Key Features (Lines 1-338)
- Account Status Check: Lines 45-48
- Pending Invoice Loading: Lines 60-130
- Payment Gateway Detection: Lines 100-125
- Pay Invoice Modal Trigger: Line 160
Display Logic:
const accountStatus = user?.account?.status;
const isPendingPayment = accountStatus === 'pending_payment';
Loads:
- Pending invoices from
/v1/billing/invoices/?status=pending&limit=1 - Account payment methods from
/v1/billing/payment-methods/ - Available gateways from
/v1/billing/stripe/config/and/v1/billing/paypal/config/
Frontend Payment Services
billing.api.ts
File: frontend/src/services/billing.api.ts
Total Lines: 1443
Key Payment Functions:
| Function | Lines | Endpoint | Description |
|---|---|---|---|
getStripeConfig |
1095-1097 | GET /v1/billing/stripe/config/ |
Get Stripe publishable key |
createStripeCheckout |
1102-1112 | POST /v1/billing/stripe/checkout/ |
Create checkout session |
createStripeCreditCheckout |
1200-1215 | POST /v1/billing/stripe/credit-checkout/ |
Credit package checkout |
openStripeBillingPortal |
1220-1230 | POST /v1/billing/stripe/billing-portal/ |
Subscription management |
getPayPalConfig |
1270-1272 | GET /v1/billing/paypal/config/ |
Get PayPal client ID |
createPayPalCreditOrder |
1278-1290 | POST /v1/billing/paypal/create-order/ |
Credit package order |
createPayPalSubscriptionOrder |
1295-1307 | POST /v1/billing/paypal/create-subscription-order/ |
Subscription order |
capturePayPalOrder |
1312-1325 | POST /v1/billing/paypal/capture-order/ |
Capture approved order |
subscribeToPlan |
1365-1380 | Gateway-specific | Unified subscribe helper |
purchaseCredits |
1385-1400 | Gateway-specific | Unified purchase helper |
getInvoices |
480-492 | GET /v1/billing/invoices/ |
List invoices |
getPayments |
510-522 | GET /v1/billing/payments/ |
List payments |
submitManualPayment |
528-542 | POST /v1/billing/payments/manual/ |
Submit bank transfer |
getAvailablePaymentMethods |
648-665 | GET /v1/billing/payment-configs/payment-methods/ |
Get payment configs |
Helper Functions (Lines 1355-1443):
// Subscribe to plan using preferred gateway
export async function subscribeToPlan(
planId: string,
gateway: PaymentGateway,
options?: { return_url?: string; cancel_url?: string }
): Promise<{ redirect_url: string }> {
switch (gateway) {
case 'stripe': return await createStripeCheckout(planId, options);
case 'paypal': return await createPayPalSubscriptionOrder(planId, options);
case 'manual': throw new Error('Use submitManualPayment()');
}
}
// Purchase credit package using preferred gateway
export async function purchaseCredits(
packageId: string,
gateway: PaymentGateway,
options?: { return_url?: string; cancel_url?: string }
): Promise<{ redirect_url: string }> {
switch (gateway) {
case 'stripe': return await createStripeCreditCheckout(packageId, options);
case 'paypal': return await createPayPalCreditOrder(packageId, options);
case 'manual': throw new Error('Use submitManualPayment()');
}
}
authStore.ts
File: frontend/src/store/authStore.ts
Total Lines: 534
Register Function (Lines 230-395):
register: async (registerData) => {
const response = await fetch(`${API_BASE_URL}/v1/auth/register/`, {
method: 'POST',
body: JSON.stringify({
...registerData,
password_confirm: registerData.password,
plan_slug: registerData.plan_slug,
}),
});
const data = await response.json();
// Extract checkout_url for payment redirect
return {
...userData,
checkout_url: responseData.checkout_url || data.checkout_url,
checkout_session_id: responseData.checkout_session_id,
paypal_order_id: responseData.paypal_order_id,
};
}
Key Return Fields:
checkout_url- Stripe/PayPal redirect URLcheckout_session_id- Stripe session IDpaypal_order_id- PayPal order ID
Backend Endpoints
Billing URLs
File: backend/igny8_core/business/billing/urls.py
# Stripe Endpoints
path('stripe/config/', StripeConfigView.as_view())
path('stripe/checkout/', StripeCheckoutView.as_view())
path('stripe/credit-checkout/', StripeCreditCheckoutView.as_view())
path('stripe/billing-portal/', StripeBillingPortalView.as_view())
path('webhooks/stripe/', stripe_webhook)
# PayPal Endpoints
path('paypal/config/', PayPalConfigView.as_view())
path('paypal/create-order/', PayPalCreateOrderView.as_view())
path('paypal/create-subscription-order/', PayPalCreateSubscriptionOrderView.as_view())
path('paypal/capture-order/', PayPalCaptureOrderView.as_view())
path('paypal/create-subscription/', PayPalCreateSubscriptionView.as_view())
path('webhooks/paypal/', paypal_webhook)
# Router ViewSets
router.register(r'invoices', InvoiceViewSet)
router.register(r'payments', PaymentViewSet)
router.register(r'credit-packages', CreditPackageViewSet)
router.register(r'payment-methods', AccountPaymentMethodViewSet)
router.register(r'payment-configs', BillingViewSet)
Stripe Views
File: backend/igny8_core/business/billing/views/stripe_views.py
Total Lines: 802
| View | Lines | Method | Endpoint | Description |
|---|---|---|---|---|
StripeConfigView |
35-55 | GET | /stripe/config/ |
Returns publishable key |
StripeCheckoutView |
58-125 | POST | /stripe/checkout/ |
Creates subscription checkout |
StripeCreditCheckoutView |
128-200 | POST | /stripe/credit-checkout/ |
Creates credit checkout |
StripeBillingPortalView |
203-260 | POST | /stripe/billing-portal/ |
Creates portal session |
stripe_webhook |
263-350 | POST | /webhooks/stripe/ |
Handles Stripe webhooks |
PayPal Views
File: backend/igny8_core/business/billing/views/paypal_views.py
Total Lines: 910
| View | Lines | Method | Endpoint | Description |
|---|---|---|---|---|
PayPalConfigView |
42-58 | GET | /paypal/config/ |
Returns client ID |
PayPalCreateOrderView |
61-135 | POST | /paypal/create-order/ |
Creates credit order |
PayPalCreateSubscriptionOrderView |
138-230 | POST | /paypal/create-subscription-order/ |
Creates subscription order |
PayPalCaptureOrderView |
233-400 | POST | /paypal/capture-order/ |
Captures approved order |
PayPalCreateSubscriptionView |
403-500 | POST | /paypal/create-subscription/ |
Creates recurring subscription |
paypal_webhook |
503-700 | POST | /webhooks/paypal/ |
Handles PayPal webhooks |
Auth Register Endpoint
File: backend/igny8_core/auth/urls.py (Lines 40-170)
Also: backend/igny8_core/auth/serializers.py (Lines 280-500)
RegisterSerializer.create() Flow (Lines 340-500):
-
Plan Lookup (Lines 340-365):
if plan_slug in ['starter', 'growth', 'scale']: plan = Plan.objects.get(slug=plan_slug, is_active=True) account_status = 'pending_payment' initial_credits = 0 else: plan = Plan.objects.get(slug='free', is_active=True) account_status = 'trial' initial_credits = plan.included_credits -
Account Creation (Lines 390-420):
account = Account.objects.create( name=account_name, owner=user, plan=plan, credits=initial_credits, status=account_status, # 'pending_payment' or 'trial' payment_method=validated_data.get('payment_method', 'bank_transfer'), billing_country=validated_data.get('billing_country', ''), ) -
For Paid Plans (Lines 445-485):
# Create Subscription subscription = Subscription.objects.create( account=account, plan=plan, status='pending_payment', current_period_start=billing_period_start, current_period_end=billing_period_end, ) # Create Invoice InvoiceService.create_subscription_invoice(subscription, ...) # Create AccountPaymentMethod AccountPaymentMethod.objects.create( account=account, type=payment_method, is_default=True, is_verified=False, )
Checkout Session Creation (RegisterView - Lines 118-165):
# For Stripe
if payment_method == 'stripe':
checkout_data = stripe_service.create_checkout_session(
account=account,
plan=account.plan,
)
response_data['checkout_url'] = checkout_data['checkout_url']
# For PayPal
elif payment_method == 'paypal':
order = paypal_service.create_order(
account=account,
amount=float(account.plan.price),
description=f'{account.plan.name} Plan Subscription',
)
response_data['checkout_url'] = order['approval_url']
Backend Services
StripeService
File: backend/igny8_core/business/billing/services/stripe_service.py
Total Lines: 628
Key Methods:
| Method | Lines | Description |
|---|---|---|
__init__ |
28-60 | Initialize from IntegrationProvider |
_get_or_create_customer |
73-110 | Get/create Stripe customer |
create_checkout_session |
130-210 | Create subscription checkout |
create_credit_checkout_session |
215-280 | Create credit package checkout |
create_billing_portal_session |
330-375 | Create portal session |
get_subscription |
380-420 | Get subscription details |
cancel_subscription |
425-450 | Cancel subscription |
construct_webhook_event |
485-500 | Verify webhook signature |
create_refund |
555-600 | Create refund |
Checkout Session Flow:
def create_checkout_session(self, account, plan, success_url=None, cancel_url=None):
customer_id = self._get_or_create_customer(account)
session = stripe.checkout.Session.create(
customer=customer_id,
payment_method_types=['card'],
mode='subscription',
line_items=[{'price': plan.stripe_price_id, 'quantity': 1}],
success_url=success_url or f'{self.frontend_url}/account/plans?success=true',
cancel_url=cancel_url or f'{self.frontend_url}/account/plans?canceled=true',
metadata={
'account_id': str(account.id),
'plan_id': str(plan.id),
'type': 'subscription',
},
)
return {'checkout_url': session.url, 'session_id': session.id}
PayPalService
File: backend/igny8_core/business/billing/services/paypal_service.py
Total Lines: 680
Key Methods:
| Method | Lines | Description |
|---|---|---|
__init__ |
44-85 | Initialize from IntegrationProvider |
_get_access_token |
105-140 | Get OAuth token |
_make_request |
145-195 | Make authenticated request |
create_order |
220-275 | Create one-time order |
create_credit_order |
300-340 | Create credit package order |
capture_order |
345-400 | Capture approved order |
create_subscription |
420-480 | Create recurring subscription |
cancel_subscription |
510-535 | Cancel subscription |
verify_webhook_signature |
565-610 | Verify webhook |
refund_capture |
615-660 | Refund payment |
Order Flow:
def create_order(self, account, amount, description='', return_url=None, cancel_url=None):
order_data = {
'intent': 'CAPTURE',
'purchase_units': [{
'amount': {'currency_code': self.currency, 'value': f'{amount:.2f}'},
'description': description or 'IGNY8 Payment',
'custom_id': str(account.id),
}],
'application_context': {
'return_url': return_url or self.return_url,
'cancel_url': cancel_url or self.cancel_url,
'brand_name': 'IGNY8',
'user_action': 'PAY_NOW',
}
}
response = self._make_request('POST', '/v2/checkout/orders', json_data=order_data)
return {
'order_id': response.get('id'),
'status': response.get('status'),
'approval_url': approval_url, # From links
}
InvoiceService
File: backend/igny8_core/business/billing/services/invoice_service.py
Total Lines: 385
Key Methods:
| Method | Lines | Description |
|---|---|---|
get_pending_invoice |
18-27 | Get pending invoice for subscription |
get_or_create_subscription_invoice |
30-50 | Avoid duplicate invoices |
generate_invoice_number |
53-80 | Generate unique invoice number |
create_subscription_invoice |
85-165 | Create subscription invoice |
create_credit_package_invoice |
170-230 | Create credit package invoice |
mark_paid |
260-290 | Mark invoice as paid |
mark_void |
295-310 | Void invoice |
Currency Logic:
# Online payments (stripe, paypal): Always USD
# Manual payments (bank_transfer, local_wallet): Local currency
if payment_method in ['stripe', 'paypal']:
currency = 'USD'
local_price = float(plan.price)
else:
currency = get_currency_for_country(account.billing_country) # 'PKR' for PK
local_price = convert_usd_to_local(float(plan.price), account.billing_country)
PaymentService
File: backend/igny8_core/business/billing/services/payment_service.py
Total Lines: 418
Key Methods:
| Method | Lines | Description |
|---|---|---|
create_stripe_payment |
17-35 | Record Stripe payment |
create_paypal_payment |
40-55 | Record PayPal payment |
create_manual_payment |
60-92 | Record manual payment (pending_approval) |
mark_payment_completed |
97-130 | Mark as succeeded |
mark_payment_failed |
135-150 | Mark as failed |
approve_manual_payment |
155-200 | Admin approves manual payment |
reject_manual_payment |
205-230 | Admin rejects manual payment |
_add_credits_for_payment |
235-270 | Add credits after payment |
get_available_payment_methods |
275-340 | Get methods by country |
Manual Payment Approval:
def approve_manual_payment(payment, approved_by_user_id, admin_notes=None):
payment.status = 'succeeded'
payment.approved_by_id = approved_by_user_id
payment.approved_at = timezone.now()
payment.save()
# Update invoice
InvoiceService.mark_paid(payment.invoice, payment_method=payment.payment_method)
# Add credits if credit package
if payment.metadata.get('credit_package_id'):
PaymentService._add_credits_for_payment(payment)
# Activate account
if account.status != 'active':
account.status = 'active'
account.save()
Models
Account Model
File: backend/igny8_core/auth/models.py (Lines 55-145)
class Account(SoftDeletableModel):
STATUS_CHOICES = [
('active', 'Active'),
('suspended', 'Suspended'),
('trial', 'Trial'),
('cancelled', 'Cancelled'),
('pending_payment', 'Pending Payment'),
]
PAYMENT_METHOD_CHOICES = [
('stripe', 'Stripe'),
('paypal', 'PayPal'),
('bank_transfer', 'Bank Transfer'),
]
name = models.CharField(max_length=255)
slug = models.SlugField(unique=True)
owner = models.ForeignKey('User', on_delete=models.SET_NULL, null=True)
stripe_customer_id = models.CharField(max_length=255, blank=True, null=True)
plan = models.ForeignKey('Plan', on_delete=models.PROTECT)
credits = models.IntegerField(default=0)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='trial')
payment_method = models.CharField(max_length=30, choices=PAYMENT_METHOD_CHOICES, default='stripe')
# Billing information
billing_email = models.EmailField(blank=True, null=True)
billing_address_line1 = models.CharField(max_length=255, blank=True)
billing_country = models.CharField(max_length=2, blank=True) # ISO country code
tax_id = models.CharField(max_length=100, blank=True)
Subscription Model
File: backend/igny8_core/auth/models.py (Lines 395-440)
class Subscription(models.Model):
STATUS_CHOICES = [
('active', 'Active'),
('past_due', 'Past Due'),
('canceled', 'Canceled'),
('trialing', 'Trialing'),
('pending_payment', 'Pending Payment'),
]
account = models.OneToOneField('Account', on_delete=models.CASCADE, related_name='subscription')
plan = models.ForeignKey('Plan', on_delete=models.PROTECT)
stripe_subscription_id = models.CharField(max_length=255, blank=True, null=True)
external_payment_id = models.CharField(max_length=255, blank=True, null=True) # PayPal/Bank ref
status = models.CharField(max_length=20, choices=STATUS_CHOICES)
current_period_start = models.DateTimeField()
current_period_end = models.DateTimeField()
cancel_at_period_end = models.BooleanField(default=False)
Plan Model
File: backend/igny8_core/auth/models.py (Lines 300-395)
class Plan(models.Model):
name = models.CharField(max_length=255)
slug = models.SlugField(unique=True)
price = models.DecimalField(max_digits=10, decimal_places=2)
billing_cycle = models.CharField(max_length=20, choices=[('monthly', 'Monthly'), ('annual', 'Annual')])
is_active = models.BooleanField(default=True)
is_internal = models.BooleanField(default=False) # Hidden from public (Free plan)
# Limits
max_users = models.IntegerField(default=1)
max_sites = models.IntegerField(default=1)
max_keywords = models.IntegerField(default=1000)
# Credits
included_credits = models.IntegerField(default=0)
# Payment Integration
stripe_product_id = models.CharField(max_length=255, blank=True, null=True)
stripe_price_id = models.CharField(max_length=255, blank=True, null=True)
Invoice Model
File: backend/igny8_core/business/billing/models.py (Lines 310-380)
class Invoice(AccountBaseModel):
STATUS_CHOICES = [
('draft', 'Draft'),
('pending', 'Pending'),
('paid', 'Paid'),
('void', 'Void'),
('uncollectible', 'Uncollectible'),
]
invoice_number = models.CharField(max_length=50, unique=True)
subscription = models.ForeignKey('Subscription', on_delete=models.SET_NULL, null=True)
subtotal = models.DecimalField(max_digits=10, decimal_places=2, default=0)
tax = models.DecimalField(max_digits=10, decimal_places=2, default=0)
total = models.DecimalField(max_digits=10, decimal_places=2, default=0)
currency = models.CharField(max_length=3, default='USD')
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
invoice_date = models.DateField()
due_date = models.DateField()
paid_at = models.DateTimeField(null=True, blank=True)
line_items = models.JSONField(default=list)
stripe_invoice_id = models.CharField(max_length=255, null=True, blank=True)
payment_method = models.CharField(max_length=50, null=True, blank=True)
metadata = models.JSONField(default=dict)
Payment Model
File: backend/igny8_core/business/billing/models.py (Lines 460-540)
class Payment(AccountBaseModel):
STATUS_CHOICES = [
('pending_approval', 'Pending Approval'), # Manual payment awaiting admin
('succeeded', 'Succeeded'),
('failed', 'Failed'),
('refunded', 'Refunded'),
]
PAYMENT_METHOD_CHOICES = [
('stripe', 'Stripe'),
('paypal', 'PayPal'),
('bank_transfer', 'Bank Transfer'),
('local_wallet', 'Local Wallet'),
('manual', 'Manual Payment'),
]
invoice = models.ForeignKey(Invoice, on_delete=models.CASCADE)
amount = models.DecimalField(max_digits=10, decimal_places=2)
currency = models.CharField(max_length=3, default='USD')
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending_approval')
payment_method = models.CharField(max_length=50, choices=PAYMENT_METHOD_CHOICES)
# Stripe
stripe_payment_intent_id = models.CharField(max_length=255, null=True, blank=True)
stripe_charge_id = models.CharField(max_length=255, null=True, blank=True)
# PayPal
paypal_order_id = models.CharField(max_length=255, null=True, blank=True)
paypal_capture_id = models.CharField(max_length=255, null=True, blank=True)
# Manual
manual_reference = models.CharField(max_length=255, blank=True) # Bank transfer ref
manual_notes = models.TextField(blank=True)
admin_notes = models.TextField(blank=True)
approved_by = models.ForeignKey('User', null=True, blank=True, on_delete=models.SET_NULL)
approved_at = models.DateTimeField(null=True, blank=True)
metadata = models.JSONField(default=dict)
AccountPaymentMethod Model
File: backend/igny8_core/business/billing/models.py (Lines 700-750)
class AccountPaymentMethod(AccountBaseModel):
"""Account-scoped payment methods (user's verified payment options)"""
PAYMENT_METHOD_CHOICES = [
('stripe', 'Stripe'),
('paypal', 'PayPal'),
('bank_transfer', 'Bank Transfer'),
('local_wallet', 'Local Wallet'),
('manual', 'Manual Payment'),
]
type = models.CharField(max_length=50, choices=PAYMENT_METHOD_CHOICES)
display_name = models.CharField(max_length=100)
is_default = models.BooleanField(default=False)
is_enabled = models.BooleanField(default=True)
is_verified = models.BooleanField(default=False) # True after successful payment
country_code = models.CharField(max_length=2, blank=True)
instructions = models.TextField(blank=True)
metadata = models.JSONField(default=dict)
PaymentMethodConfig Model
File: backend/igny8_core/business/billing/models.py (Lines 600-695)
class PaymentMethodConfig(models.Model):
"""System-level payment method configuration per country"""
PAYMENT_METHOD_CHOICES = [
('stripe', 'Stripe'),
('paypal', 'PayPal'),
('bank_transfer', 'Bank Transfer'),
('local_wallet', 'Local Wallet'),
('manual', 'Manual Payment'),
]
country_code = models.CharField(max_length=2) # 'US', 'PK', '*' for global
payment_method = models.CharField(max_length=50, choices=PAYMENT_METHOD_CHOICES)
is_enabled = models.BooleanField(default=True)
display_name = models.CharField(max_length=100, blank=True)
instructions = models.TextField(blank=True)
# Bank Transfer Details
bank_name = models.CharField(max_length=255, blank=True)
account_number = models.CharField(max_length=255, blank=True)
account_title = models.CharField(max_length=255, blank=True)
routing_number = models.CharField(max_length=255, blank=True)
swift_code = models.CharField(max_length=255, blank=True)
iban = models.CharField(max_length=255, blank=True)
# Local Wallet Details
wallet_type = models.CharField(max_length=100, blank=True) # JazzCash, EasyPaisa
wallet_id = models.CharField(max_length=255, blank=True)
sort_order = models.IntegerField(default=0)
class Meta:
unique_together = [['country_code', 'payment_method']]
Payment Method Configuration
Country-Based Payment Methods
| Country | Stripe | PayPal | Bank Transfer | Local Wallet |
|---|---|---|---|---|
Global (*) |
✅ | ✅ | ❌ | ❌ |
Pakistan (PK) |
✅ | ❌ | ✅ | ✅ (JazzCash, EasyPaisa) |
Configuration Flow
- PaymentMethodConfig stores system-level configs per country
- AccountPaymentMethod stores user's saved/verified methods
- Frontend queries
/v1/billing/payment-configs/payment-methods/to get available options - Frontend filters based on user's country (from
countryCodeprop or account'sbilling_country)
Payment Flows
Flow 1: Signup with Stripe
sequenceDiagram
participant U as User
participant F as Frontend
participant B as Backend
participant S as Stripe
U->>F: Fill signup form, select Stripe
F->>B: POST /v1/auth/register/ {payment_method: 'stripe', plan_slug: 'starter'}
B->>B: Create User, Account (status=pending_payment), Subscription, Invoice
B->>S: Create Checkout Session
S-->>B: checkout_url
B-->>F: {user, tokens, checkout_url}
F->>S: Redirect to checkout_url
U->>S: Complete payment
S->>B: POST /webhooks/stripe/ (checkout.session.completed)
B->>B: Activate subscription, mark invoice paid, add credits
S->>F: Redirect to success_url
F->>B: GET /v1/auth/me/ (refresh user)
Flow 2: Signup with Bank Transfer
sequenceDiagram
participant U as User
participant F as Frontend
participant B as Backend
participant A as Admin
U->>F: Fill signup form, select Bank Transfer
F->>B: POST /v1/auth/register/ {payment_method: 'bank_transfer'}
B->>B: Create User, Account (status=pending_payment), Subscription, Invoice, AccountPaymentMethod
B-->>F: {user, tokens}
F->>F: Navigate to /account/plans (shows PendingPaymentBanner)
U->>F: Make bank transfer, enter reference
F->>B: POST /v1/billing/admin/payments/confirm/ {invoice_id, manual_reference}
B->>B: Create Payment (status=pending_approval)
A->>B: POST /v1/admin/billing/{id}/approve_payment/
B->>B: Mark payment succeeded, invoice paid, activate account, add credits
Flow 3: Credit Purchase with PayPal
sequenceDiagram
participant U as User
participant F as Frontend
participant B as Backend
participant P as PayPal
U->>F: Click Buy Credits, select PayPal
F->>B: POST /v1/billing/paypal/create-order/ {package_id}
B->>P: Create Order
P-->>B: {order_id, approval_url}
B-->>F: {order_id, approval_url}
F->>P: Redirect to approval_url
U->>P: Approve payment
P->>F: Redirect to return_url with token
F->>B: POST /v1/billing/paypal/capture-order/ {order_id}
B->>P: Capture Order
P-->>B: {capture_id, status: COMPLETED}
B->>B: Create invoice, payment, add credits
B-->>F: {credits_added, new_balance}
Flow 4: Pay Pending Invoice (PayInvoiceModal)
sequenceDiagram
participant U as User
participant F as Frontend
participant B as Backend
participant S as Stripe
U->>F: Click "Pay Invoice" button
F->>F: Open PayInvoiceModal
U->>F: Select Stripe, click Pay
F->>B: POST /v1/billing/stripe/checkout/ {plan_id}
B->>S: Create Checkout Session
S-->>B: checkout_url
B-->>F: {checkout_url}
F->>S: Redirect to checkout_url
U->>S: Complete payment
S->>B: Webhook: checkout.session.completed
B->>B: Update subscription, invoice, account status
Webhooks
Stripe Webhooks
Endpoint: POST /v1/billing/webhooks/stripe/
| Event | Handler | Action |
|---|---|---|
checkout.session.completed |
_handle_checkout_completed |
Activate subscription OR add purchased credits |
invoice.paid |
_handle_invoice_paid |
Add monthly credits for renewal |
invoice.payment_failed |
_handle_payment_failed |
Send notification, update status |
customer.subscription.updated |
_handle_subscription_updated |
Sync plan changes |
customer.subscription.deleted |
_handle_subscription_deleted |
Cancel subscription |
Checkout Completed Handler (Lines 355-475):
def _handle_checkout_completed(session):
metadata = session.get('metadata', {})
account_id = metadata.get('account_id')
mode = session.get('mode')
if mode == 'subscription':
_activate_subscription(account, subscription_id, plan_id, session)
elif mode == 'payment':
_add_purchased_credits(account, credit_package_id, credit_amount, session)
PayPal Webhooks
Endpoint: POST /v1/billing/webhooks/paypal/
| Event | Action |
|---|---|
CHECKOUT.ORDER.APPROVED |
Auto-capture (if configured) |
PAYMENT.CAPTURE.COMPLETED |
Mark payment succeeded, add credits |
PAYMENT.CAPTURE.DENIED |
Mark payment failed |
BILLING.SUBSCRIPTION.ACTIVATED |
Activate subscription |
BILLING.SUBSCRIPTION.CANCELLED |
Cancel subscription |
BILLING.SUBSCRIPTION.PAYMENT.FAILED |
Handle failed renewal |
Summary
Key Files Reference
| Category | File | Lines |
|---|---|---|
| Frontend Entry Points | ||
| Signup Form | frontend/src/components/auth/SignUpFormUnified.tsx |
770 |
| Plans & Billing Page | frontend/src/pages/account/PlansAndBillingPage.tsx |
1197 |
| Pay Invoice Modal | frontend/src/components/billing/PayInvoiceModal.tsx |
544 |
| Pending Payment Banner | frontend/src/components/billing/PendingPaymentBanner.tsx |
338 |
| Frontend Services | ||
| Billing API | frontend/src/services/billing.api.ts |
1443 |
| Auth Store | frontend/src/store/authStore.ts |
534 |
| Backend Views | ||
| Stripe Views | backend/igny8_core/business/billing/views/stripe_views.py |
802 |
| PayPal Views | backend/igny8_core/business/billing/views/paypal_views.py |
910 |
| Backend Services | ||
| Stripe Service | backend/igny8_core/business/billing/services/stripe_service.py |
628 |
| PayPal Service | backend/igny8_core/business/billing/services/paypal_service.py |
680 |
| Invoice Service | backend/igny8_core/business/billing/services/invoice_service.py |
385 |
| Payment Service | backend/igny8_core/business/billing/services/payment_service.py |
418 |
| Models | ||
| Auth Models | backend/igny8_core/auth/models.py |
866 |
| Billing Models | backend/igny8_core/business/billing/models.py |
857 |
| Serializers | ||
| Auth Serializers | backend/igny8_core/auth/serializers.py |
561 |
| URLs | ||
| Billing URLs | backend/igny8_core/business/billing/urls.py |
73 |
| Auth URLs | backend/igny8_core/auth/urls.py |
461 |
Quick Reference: API Endpoints
Stripe
GET /v1/billing/stripe/config/ - Get publishable key
POST /v1/billing/stripe/checkout/ - Create subscription checkout
POST /v1/billing/stripe/credit-checkout/ - Create credit checkout
POST /v1/billing/stripe/billing-portal/ - Open billing portal
POST /v1/billing/webhooks/stripe/ - Webhook handler
PayPal
GET /v1/billing/paypal/config/ - Get client ID
POST /v1/billing/paypal/create-order/ - Create credit order
POST /v1/billing/paypal/create-subscription-order/ - Create subscription order
POST /v1/billing/paypal/capture-order/ - Capture approved order
POST /v1/billing/paypal/create-subscription/ - Create recurring subscription
POST /v1/billing/webhooks/paypal/ - Webhook handler
Invoices & Payments
GET /v1/billing/invoices/ - List invoices
GET /v1/billing/invoices/{id}/ - Get invoice detail
GET /v1/billing/invoices/{id}/download_pdf/ - Download PDF
GET /v1/billing/payments/ - List payments
POST /v1/billing/payments/manual/ - Submit manual payment
Admin Payment Management
GET /v1/admin/billing/pending_payments/ - List pending approvals
POST /v1/admin/billing/{id}/approve_payment/ - Approve payment
POST /v1/admin/billing/{id}/reject_payment/ - Reject payment
Payment Methods
GET /v1/billing/payment-methods/ - Get user's payment methods
POST /v1/billing/payment-methods/ - Create payment method
POST /v1/billing/payment-methods/{id}/set_default/ - Set default
GET /v1/billing/payment-configs/payment-methods/ - Get available configs