docs and billing adn acaoutn 40%

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-04 23:56:38 +00:00
parent 1e3299a089
commit 3a7ea1f4f3
21 changed files with 4994 additions and 24 deletions

View File

@@ -0,0 +1,53 @@
# Generated by Django 5.2.8 on 2025-12-04 23:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('igny8_core_auth', '0003_add_sync_event_model'),
]
operations = [
migrations.AddField(
model_name='account',
name='billing_address_line1',
field=models.CharField(blank=True, help_text='Street address', max_length=255),
),
migrations.AddField(
model_name='account',
name='billing_address_line2',
field=models.CharField(blank=True, help_text='Apt, suite, etc.', max_length=255),
),
migrations.AddField(
model_name='account',
name='billing_city',
field=models.CharField(blank=True, max_length=100),
),
migrations.AddField(
model_name='account',
name='billing_country',
field=models.CharField(blank=True, help_text='ISO 2-letter country code', max_length=2),
),
migrations.AddField(
model_name='account',
name='billing_email',
field=models.EmailField(blank=True, help_text='Email for billing notifications', max_length=254, null=True),
),
migrations.AddField(
model_name='account',
name='billing_postal_code',
field=models.CharField(blank=True, max_length=20),
),
migrations.AddField(
model_name='account',
name='billing_state',
field=models.CharField(blank=True, help_text='State/Province/Region', max_length=100),
),
migrations.AddField(
model_name='account',
name='tax_id',
field=models.CharField(blank=True, help_text='VAT/Tax ID number', max_length=100),
),
]

View File

@@ -70,6 +70,17 @@ class Account(models.Model):
plan = models.ForeignKey('igny8_core_auth.Plan', on_delete=models.PROTECT, related_name='accounts')
credits = models.IntegerField(default=0, validators=[MinValueValidator(0)])
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='trial')
# Billing information
billing_email = models.EmailField(blank=True, null=True, help_text="Email for billing notifications")
billing_address_line1 = models.CharField(max_length=255, blank=True, help_text="Street address")
billing_address_line2 = models.CharField(max_length=255, blank=True, help_text="Apt, suite, etc.")
billing_city = models.CharField(max_length=100, blank=True)
billing_state = models.CharField(max_length=100, blank=True, help_text="State/Province/Region")
billing_postal_code = models.CharField(max_length=20, blank=True)
billing_country = models.CharField(max_length=2, blank=True, help_text="ISO 2-letter country code")
tax_id = models.CharField(max_length=100, blank=True, help_text="VAT/Tax ID number")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

View File

@@ -158,3 +158,249 @@ class CreditCostConfig(models.Model):
except CreditCostConfig.DoesNotExist:
pass
super().save(*args, **kwargs)
class Invoice(AccountBaseModel):
"""
Invoice for subscription or credit purchases
Tracks billing invoices with line items and payment status
"""
STATUS_CHOICES = [
('draft', 'Draft'),
('pending', 'Pending'),
('paid', 'Paid'),
('void', 'Void'),
('uncollectible', 'Uncollectible'),
]
invoice_number = models.CharField(max_length=50, unique=True, db_index=True)
subscription = models.ForeignKey(
'igny8_core_auth.Subscription',
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='invoices'
)
# Amounts
subtotal = models.DecimalField(max_digits=10, decimal_places=2)
tax = models.DecimalField(max_digits=10, decimal_places=2, default=0)
total = models.DecimalField(max_digits=10, decimal_places=2)
# Status
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', db_index=True)
# Dates
invoice_date = models.DateField(db_index=True)
due_date = models.DateField()
paid_at = models.DateTimeField(null=True, blank=True)
# Line items
line_items = models.JSONField(default=list, help_text="Invoice line items: [{description, amount, quantity}]")
# Payment integration
stripe_invoice_id = models.CharField(max_length=255, null=True, blank=True)
payment_method = models.CharField(max_length=50, null=True, blank=True)
# Metadata
notes = models.TextField(blank=True)
metadata = models.JSONField(default=dict)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
app_label = 'billing'
db_table = 'igny8_invoices'
ordering = ['-invoice_date', '-created_at']
indexes = [
models.Index(fields=['account', 'status']),
models.Index(fields=['account', 'invoice_date']),
models.Index(fields=['invoice_number']),
]
def __str__(self):
return f"Invoice {self.invoice_number} - {self.account.name if self.account else 'No Account'}"
class Payment(AccountBaseModel):
"""
Payment record for invoices
Supports: Stripe, PayPal, Manual (Bank Transfer, Local Wallet)
"""
STATUS_CHOICES = [
('pending', 'Pending'),
('processing', 'Processing'),
('succeeded', 'Succeeded'),
('failed', 'Failed'),
('refunded', 'Refunded'),
('cancelled', 'Cancelled'),
]
PAYMENT_METHOD_CHOICES = [
('stripe', 'Stripe (Credit/Debit Card)'),
('paypal', 'PayPal'),
('bank_transfer', 'Bank Transfer (Manual)'),
('local_wallet', 'Local Wallet (Manual)'),
('manual', 'Manual Payment'),
]
invoice = models.ForeignKey(Invoice, on_delete=models.CASCADE, related_name='payments')
# Amount
amount = models.DecimalField(max_digits=10, decimal_places=2)
currency = models.CharField(max_length=3, default='USD')
# Status
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', db_index=True)
# Payment method
payment_method = models.CharField(max_length=50, choices=PAYMENT_METHOD_CHOICES, db_index=True)
# Stripe integration
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 integration
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 payment details
manual_reference = models.CharField(
max_length=255,
blank=True,
help_text="Bank transfer reference, wallet transaction ID, etc."
)
manual_notes = models.TextField(blank=True, help_text="Admin notes for manual payments")
approved_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='approved_payments'
)
approved_at = models.DateTimeField(null=True, blank=True)
# Timestamps
processed_at = models.DateTimeField(null=True, blank=True)
failed_at = models.DateTimeField(null=True, blank=True)
refunded_at = models.DateTimeField(null=True, blank=True)
# Error tracking
failure_reason = models.TextField(blank=True)
# Metadata
metadata = models.JSONField(default=dict)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
app_label = 'billing'
db_table = 'igny8_payments'
ordering = ['-created_at']
indexes = [
models.Index(fields=['account', 'status']),
models.Index(fields=['account', 'payment_method']),
models.Index(fields=['invoice', 'status']),
]
def __str__(self):
return f"Payment {self.id} - {self.get_payment_method_display()} - {self.amount} {self.currency}"
class CreditPackage(models.Model):
"""
One-time credit purchase packages
Defines available credit bundles for purchase
"""
name = models.CharField(max_length=100)
slug = models.SlugField(unique=True, db_index=True)
# Credits
credits = models.IntegerField(validators=[MinValueValidator(1)])
# Pricing
price = models.DecimalField(max_digits=10, decimal_places=2)
discount_percentage = models.IntegerField(default=0, help_text="Discount percentage (0-100)")
# Stripe
stripe_product_id = models.CharField(max_length=255, null=True, blank=True)
stripe_price_id = models.CharField(max_length=255, null=True, blank=True)
# PayPal
paypal_plan_id = models.CharField(max_length=255, null=True, blank=True)
# Status
is_active = models.BooleanField(default=True, db_index=True)
is_featured = models.BooleanField(default=False, help_text="Show as featured package")
# Display
description = models.TextField(blank=True)
features = models.JSONField(default=list, help_text="Bonus features or highlights")
# Sort order
sort_order = models.IntegerField(default=0, help_text="Display order (lower = first)")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
app_label = 'billing'
db_table = 'igny8_credit_packages'
ordering = ['sort_order', 'price']
def __str__(self):
return f"{self.name} - {self.credits} credits - ${self.price}"
class PaymentMethodConfig(models.Model):
"""
Configure payment methods availability per country
Allows enabling/disabling manual payments by region
"""
PAYMENT_METHOD_CHOICES = [
('stripe', 'Stripe'),
('paypal', 'PayPal'),
('bank_transfer', 'Bank Transfer'),
('local_wallet', 'Local Wallet'),
]
country_code = models.CharField(
max_length=2,
db_index=True,
help_text="ISO 2-letter country code (e.g., US, GB, IN)"
)
payment_method = models.CharField(max_length=50, choices=PAYMENT_METHOD_CHOICES)
is_enabled = models.BooleanField(default=True)
# Display info
display_name = models.CharField(max_length=100, blank=True)
instructions = models.TextField(blank=True, help_text="Payment instructions for users")
# Manual payment details (for bank_transfer/local_wallet)
bank_name = models.CharField(max_length=255, blank=True)
account_number = 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)
# Additional fields for local wallets
wallet_type = models.CharField(max_length=100, blank=True, help_text="E.g., PayTM, PhonePe, etc.")
wallet_id = models.CharField(max_length=255, blank=True)
# Order/priority
sort_order = models.IntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
app_label = 'billing'
db_table = 'igny8_payment_method_config'
unique_together = [['country_code', 'payment_method']]
ordering = ['country_code', 'sort_order']
verbose_name = 'Payment Method Configuration'
verbose_name_plural = 'Payment Method Configurations'
def __str__(self):
return f"{self.country_code} - {self.get_payment_method_display()}"

View File

@@ -0,0 +1,249 @@
"""
Invoice Service - Handles invoice creation, management, and PDF generation
"""
from decimal import Decimal
from datetime import datetime, timedelta
from typing import Dict, List, Optional
from django.db import transaction
from django.utils import timezone
from ..models import Invoice, CreditPackage
from ....auth.models import Account, Subscription
class InvoiceService:
"""Service for managing invoices"""
@staticmethod
def generate_invoice_number(account: Account) -> str:
"""
Generate unique invoice number
Format: INV-{ACCOUNT_ID}-{YEAR}{MONTH}-{COUNTER}
"""
now = timezone.now()
prefix = f"INV-{account.id}-{now.year}{now.month:02d}"
# Get count of invoices for this account this month
count = Invoice.objects.filter(
account=account,
created_at__year=now.year,
created_at__month=now.month
).count()
return f"{prefix}-{count + 1:04d}"
@staticmethod
@transaction.atomic
def create_subscription_invoice(
subscription: Subscription,
billing_period_start: datetime,
billing_period_end: datetime
) -> Invoice:
"""
Create invoice for subscription billing period
"""
account = subscription.account
plan = subscription.plan
invoice = Invoice.objects.create(
account=account,
subscription=subscription,
invoice_number=InvoiceService.generate_invoice_number(account),
billing_email=account.billing_email or account.users.filter(role='owner').first().email,
status='pending',
currency='USD',
billing_period_start=billing_period_start,
billing_period_end=billing_period_end
)
# Add line item for subscription
invoice.add_line_item(
description=f"{plan.name} Plan - {billing_period_start.strftime('%b %Y')}",
quantity=1,
unit_price=plan.price,
amount=plan.price
)
invoice.calculate_totals()
invoice.save()
return invoice
@staticmethod
@transaction.atomic
def create_credit_package_invoice(
account: Account,
credit_package: CreditPackage
) -> Invoice:
"""
Create invoice for credit package purchase
"""
invoice = Invoice.objects.create(
account=account,
invoice_number=InvoiceService.generate_invoice_number(account),
billing_email=account.billing_email or account.users.filter(role='owner').first().email,
status='pending',
currency='USD'
)
# Add line item for credit package
invoice.add_line_item(
description=f"{credit_package.name} - {credit_package.credits:,} Credits",
quantity=1,
unit_price=credit_package.price,
amount=credit_package.price
)
invoice.calculate_totals()
invoice.save()
return invoice
@staticmethod
@transaction.atomic
def create_custom_invoice(
account: Account,
line_items: List[Dict],
billing_email: Optional[str] = None,
notes: Optional[str] = None,
due_date: Optional[datetime] = None
) -> Invoice:
"""
Create custom invoice with multiple line items
Args:
account: Account to bill
line_items: List of dicts with keys: description, quantity, unit_price
billing_email: Override billing email
notes: Invoice notes
due_date: Payment due date
"""
invoice = Invoice.objects.create(
account=account,
invoice_number=InvoiceService.generate_invoice_number(account),
billing_email=billing_email or account.billing_email or account.users.filter(role='owner').first().email,
status='draft',
currency='USD',
notes=notes,
due_date=due_date or (timezone.now() + timedelta(days=30))
)
# Add all line items
for item in line_items:
invoice.add_line_item(
description=item['description'],
quantity=item.get('quantity', 1),
unit_price=Decimal(str(item['unit_price'])),
amount=Decimal(str(item.get('amount', item['quantity'] * item['unit_price'])))
)
invoice.calculate_totals()
invoice.save()
return invoice
@staticmethod
@transaction.atomic
def mark_paid(
invoice: Invoice,
payment_method: str,
transaction_id: Optional[str] = None
) -> Invoice:
"""
Mark invoice as paid
"""
invoice.status = 'paid'
invoice.paid_at = timezone.now()
invoice.save()
return invoice
@staticmethod
@transaction.atomic
def mark_void(invoice: Invoice, reason: Optional[str] = None) -> Invoice:
"""
Void an invoice
"""
if invoice.status == 'paid':
raise ValueError("Cannot void a paid invoice")
invoice.status = 'void'
if reason:
invoice.notes = f"{invoice.notes}\n\nVoided: {reason}" if invoice.notes else f"Voided: {reason}"
invoice.save()
return invoice
@staticmethod
def generate_pdf(invoice: Invoice) -> bytes:
"""
Generate PDF for invoice
TODO: Implement PDF generation using reportlab or weasyprint
For now, return placeholder
"""
from io import BytesIO
# Placeholder - implement PDF generation
buffer = BytesIO()
# Simple text representation for now
content = f"""
INVOICE #{invoice.invoice_number}
Bill To: {invoice.account.name}
Email: {invoice.billing_email}
Date: {invoice.created_at.strftime('%Y-%m-%d')}
Due Date: {invoice.due_date.strftime('%Y-%m-%d') if invoice.due_date else 'N/A'}
Line Items:
"""
for item in invoice.line_items:
content += f" {item['description']} - ${item['amount']}\n"
content += f"""
Subtotal: ${invoice.subtotal}
Tax: ${invoice.tax_amount}
Total: ${invoice.total_amount}
Status: {invoice.status.upper()}
"""
buffer.write(content.encode('utf-8'))
buffer.seek(0)
return buffer.getvalue()
@staticmethod
def get_account_invoices(
account: Account,
status: Optional[str] = None,
limit: int = 50
) -> List[Invoice]:
"""
Get invoices for an account
"""
queryset = Invoice.objects.filter(account=account)
if status:
queryset = queryset.filter(status=status)
return list(queryset.order_by('-created_at')[:limit])
@staticmethod
def get_upcoming_renewals(days: int = 7) -> List[Subscription]:
"""
Get subscriptions that will renew in the next N days
"""
from django.utils import timezone
from datetime import timedelta
cutoff_date = timezone.now() + timedelta(days=days)
return list(
Subscription.objects.filter(
status='active',
current_period_end__lte=cutoff_date
).select_related('account', 'plan')
)

View File

@@ -0,0 +1,375 @@
"""
Payment Service - Handles payment processing across multiple gateways
"""
from decimal import Decimal
from typing import Optional, Dict, Any
from django.db import transaction
from django.utils import timezone
from ..models import Payment, Invoice, CreditPackage, PaymentMethodConfig, CreditTransaction
from ....auth.models import Account
class PaymentService:
"""Service for processing payments across multiple gateways"""
@staticmethod
@transaction.atomic
def create_stripe_payment(
invoice: Invoice,
stripe_payment_intent_id: str,
stripe_charge_id: Optional[str] = None,
metadata: Optional[Dict] = None
) -> Payment:
"""
Create payment record for Stripe transaction
"""
payment = Payment.objects.create(
account=invoice.account,
invoice=invoice,
amount=invoice.total_amount,
currency=invoice.currency,
payment_method='stripe',
status='pending',
stripe_payment_intent_id=stripe_payment_intent_id,
stripe_charge_id=stripe_charge_id,
metadata=metadata or {}
)
return payment
@staticmethod
@transaction.atomic
def create_paypal_payment(
invoice: Invoice,
paypal_order_id: str,
metadata: Optional[Dict] = None
) -> Payment:
"""
Create payment record for PayPal transaction
"""
payment = Payment.objects.create(
account=invoice.account,
invoice=invoice,
amount=invoice.total_amount,
currency=invoice.currency,
payment_method='paypal',
status='pending',
paypal_order_id=paypal_order_id,
metadata=metadata or {}
)
return payment
@staticmethod
@transaction.atomic
def create_manual_payment(
invoice: Invoice,
payment_method: str, # 'bank_transfer' or 'local_wallet'
transaction_reference: str,
admin_notes: Optional[str] = None,
metadata: Optional[Dict] = None
) -> Payment:
"""
Create manual payment (bank transfer or local wallet)
Requires admin approval
"""
if payment_method not in ['bank_transfer', 'local_wallet', 'manual']:
raise ValueError("Invalid manual payment method")
payment = Payment.objects.create(
account=invoice.account,
invoice=invoice,
amount=invoice.total_amount,
currency=invoice.currency,
payment_method=payment_method,
status='pending_approval',
transaction_reference=transaction_reference,
admin_notes=admin_notes,
metadata=metadata or {}
)
return payment
@staticmethod
@transaction.atomic
def mark_payment_completed(
payment: Payment,
transaction_id: Optional[str] = None
) -> Payment:
"""
Mark payment as completed and update invoice
"""
from .invoice_service import InvoiceService
payment.status = 'completed'
payment.processed_at = timezone.now()
if transaction_id:
payment.transaction_reference = transaction_id
payment.save()
# Update invoice
if payment.invoice:
InvoiceService.mark_paid(
payment.invoice,
payment_method=payment.payment_method,
transaction_id=transaction_id
)
# If payment is for credit package, add credits to account
if payment.metadata.get('credit_package_id'):
PaymentService._add_credits_for_payment(payment)
return payment
@staticmethod
@transaction.atomic
def mark_payment_failed(
payment: Payment,
failure_reason: Optional[str] = None
) -> Payment:
"""
Mark payment as failed
"""
payment.status = 'failed'
payment.failure_reason = failure_reason
payment.processed_at = timezone.now()
payment.save()
return payment
@staticmethod
@transaction.atomic
def approve_manual_payment(
payment: Payment,
approved_by_user_id: int,
admin_notes: Optional[str] = None
) -> Payment:
"""
Approve manual payment (admin action)
"""
if payment.status != 'pending_approval':
raise ValueError("Payment is not pending approval")
payment.status = 'completed'
payment.processed_at = timezone.now()
payment.approved_by_id = approved_by_user_id
if admin_notes:
payment.admin_notes = f"{payment.admin_notes}\n\nApproval notes: {admin_notes}" if payment.admin_notes else admin_notes
payment.save()
# Update invoice
if payment.invoice:
from .invoice_service import InvoiceService
InvoiceService.mark_paid(
payment.invoice,
payment_method=payment.payment_method,
transaction_id=payment.transaction_reference
)
# If payment is for credit package, add credits
if payment.metadata.get('credit_package_id'):
PaymentService._add_credits_for_payment(payment)
return payment
@staticmethod
@transaction.atomic
def reject_manual_payment(
payment: Payment,
rejected_by_user_id: int,
rejection_reason: str
) -> Payment:
"""
Reject manual payment (admin action)
"""
if payment.status != 'pending_approval':
raise ValueError("Payment is not pending approval")
payment.status = 'failed'
payment.failure_reason = rejection_reason
payment.processed_at = timezone.now()
payment.admin_notes = f"{payment.admin_notes}\n\nRejected by user {rejected_by_user_id}: {rejection_reason}" if payment.admin_notes else f"Rejected: {rejection_reason}"
payment.save()
return payment
@staticmethod
def _add_credits_for_payment(payment: Payment) -> None:
"""
Add credits to account after successful payment
"""
credit_package_id = payment.metadata.get('credit_package_id')
if not credit_package_id:
return
try:
credit_package = CreditPackage.objects.get(id=credit_package_id)
except CreditPackage.DoesNotExist:
return
# Create credit transaction
CreditTransaction.objects.create(
account=payment.account,
amount=credit_package.credits,
transaction_type='purchase',
description=f"Purchased {credit_package.name}",
reference_id=str(payment.id),
metadata={
'payment_id': payment.id,
'credit_package_id': credit_package_id,
'invoice_id': payment.invoice_id if payment.invoice else None
}
)
@staticmethod
def get_available_payment_methods(account: Account) -> Dict[str, Any]:
"""
Get available payment methods for account's country
"""
country_code = account.billing_country or 'US'
# Get payment method configurations for country
configs = PaymentMethodConfig.objects.filter(
country_code=country_code,
is_enabled=True
).order_by('sort_order')
# Default methods if no config
if not configs.exists():
return {
'methods': [
{
'type': 'stripe',
'name': 'Credit/Debit Card',
'instructions': 'Pay securely with your credit or debit card'
},
{
'type': 'paypal',
'name': 'PayPal',
'instructions': 'Pay with your PayPal account'
}
],
'stripe': True,
'paypal': True,
'bank_transfer': False,
'local_wallet': False
}
# Build response from configs
methods = []
method_flags = {
'stripe': False,
'paypal': False,
'bank_transfer': False,
'local_wallet': False
}
for config in configs:
method_flags[config.payment_method] = True
method_data = {
'type': config.payment_method,
'name': config.display_name or config.get_payment_method_display(),
'instructions': config.instructions
}
# Add bank details if bank_transfer
if config.payment_method == 'bank_transfer' and config.bank_name:
method_data['bank_details'] = {
'bank_name': config.bank_name,
'account_number': config.account_number,
'routing_number': config.routing_number,
'swift_code': config.swift_code
}
# Add wallet details if local_wallet
if config.payment_method == 'local_wallet' and config.wallet_type:
method_data['wallet_details'] = {
'wallet_type': config.wallet_type,
'wallet_id': config.wallet_id
}
methods.append(method_data)
return {
'methods': methods,
**method_flags
}
@staticmethod
def get_pending_approvals() -> list:
"""
Get all payments pending admin approval
"""
return list(
Payment.objects.filter(
status='pending_approval'
).select_related('account', 'invoice').order_by('-created_at')
)
@staticmethod
def refund_payment(
payment: Payment,
amount: Optional[Decimal] = None,
reason: Optional[str] = None
) -> Payment:
"""
Process refund for a payment
TODO: Implement actual refund logic for Stripe/PayPal
For now, just mark as refunded
"""
if payment.status != 'completed':
raise ValueError("Can only refund completed payments")
refund_amount = amount or payment.amount
if refund_amount > payment.amount:
raise ValueError("Refund amount cannot exceed payment amount")
# Create refund payment record
refund = Payment.objects.create(
account=payment.account,
invoice=payment.invoice,
amount=-refund_amount, # Negative amount for refund
currency=payment.currency,
payment_method=payment.payment_method,
status='completed',
processed_at=timezone.now(),
metadata={
'refund_for_payment_id': payment.id,
'refund_reason': reason,
'original_amount': str(payment.amount)
}
)
# Update original payment metadata
payment.metadata['refunded'] = True
payment.metadata['refund_payment_id'] = refund.id
payment.metadata['refund_amount'] = str(refund_amount)
payment.save()
return refund
@staticmethod
def get_account_payments(
account: Account,
status: Optional[str] = None,
limit: int = 50
) -> list:
"""
Get payment history for account
"""
queryset = Payment.objects.filter(account=account)
if status:
queryset = queryset.filter(status=status)
return list(
queryset.select_related('invoice')
.order_by('-created_at')[:limit]
)

View File

@@ -0,0 +1,23 @@
"""
URL patterns for business billing module (invoices, payments, credit packages)
"""
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import (
InvoiceViewSet,
PaymentViewSet,
CreditPackageViewSet,
CreditTransactionViewSet,
AdminBillingViewSet
)
router = DefaultRouter()
router.register(r'invoices', InvoiceViewSet, basename='invoice')
router.register(r'payments', PaymentViewSet, basename='payment')
router.register(r'credit-packages', CreditPackageViewSet, basename='credit-package')
router.register(r'transactions', CreditTransactionViewSet, basename='transaction')
router.register(r'admin', AdminBillingViewSet, basename='admin-billing')
urlpatterns = [
path('', include(router.urls)),
]

View File

@@ -1,54 +1,410 @@
"""
Billing API Views
Stub endpoints for billing pages
Comprehensive billing endpoints for invoices, payments, credit packages
"""
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from .models import Invoice, Payment, CreditPackage, PaymentMethodConfig, CreditTransaction
from .services.invoice_service import InvoiceService
from .services.payment_service import PaymentService
class BillingViewSet(viewsets.ViewSet):
"""Billing endpoints"""
class InvoiceViewSet(viewsets.ViewSet):
"""Invoice management endpoints"""
permission_classes = [IsAuthenticated]
@action(detail=False, methods=['get'], url_path='account_balance')
def account_balance(self, request):
"""Get user's credit balance"""
def list(self, request):
"""List invoices for current account"""
account = request.user.account
status_filter = request.query_params.get('status')
invoices = InvoiceService.get_account_invoices(
account=account,
status=status_filter
)
return Response({
'credits': 0,
'subscription_plan': 'Free',
'monthly_credits_included': 0,
'bonus_credits': 0
'results': [
{
'id': inv.id,
'invoice_number': inv.invoice_number,
'status': inv.status,
'total_amount': str(inv.total_amount),
'subtotal': str(inv.subtotal),
'tax_amount': str(inv.tax_amount),
'currency': inv.currency,
'created_at': inv.created_at.isoformat(),
'paid_at': inv.paid_at.isoformat() if inv.paid_at else None,
'due_date': inv.due_date.isoformat() if inv.due_date else None,
'line_items': inv.line_items,
'billing_period_start': inv.billing_period_start.isoformat() if inv.billing_period_start else None,
'billing_period_end': inv.billing_period_end.isoformat() if inv.billing_period_end else None
}
for inv in invoices
],
'count': len(invoices)
})
def retrieve(self, request, pk=None):
"""Get invoice details"""
account = request.user.account
invoice = get_object_or_404(Invoice, id=pk, account=account)
return Response({
'id': invoice.id,
'invoice_number': invoice.invoice_number,
'status': invoice.status,
'total_amount': str(invoice.total_amount),
'subtotal': str(invoice.subtotal),
'tax_amount': str(invoice.tax_amount),
'currency': invoice.currency,
'created_at': invoice.created_at.isoformat(),
'paid_at': invoice.paid_at.isoformat() if invoice.paid_at else None,
'due_date': invoice.due_date.isoformat() if invoice.due_date else None,
'line_items': invoice.line_items,
'billing_email': invoice.billing_email,
'notes': invoice.notes,
'stripe_invoice_id': invoice.stripe_invoice_id,
'billing_period_start': invoice.billing_period_start.isoformat() if invoice.billing_period_start else None,
'billing_period_end': invoice.billing_period_end.isoformat() if invoice.billing_period_end else None
})
@action(detail=True, methods=['get'])
def download_pdf(self, request, pk=None):
"""Download invoice as PDF"""
account = request.user.account
invoice = get_object_or_404(Invoice, id=pk, account=account)
pdf_data = InvoiceService.generate_pdf(invoice)
response = HttpResponse(pdf_data, content_type='application/pdf')
response['Content-Disposition'] = f'attachment; filename="invoice-{invoice.invoice_number}.pdf"'
return response
class PaymentViewSet(viewsets.ViewSet):
"""Payment processing endpoints"""
permission_classes = [IsAuthenticated]
def list(self, request):
"""List payments for current account"""
account = request.user.account
status_filter = request.query_params.get('status')
payments = PaymentService.get_account_payments(
account=account,
status=status_filter
)
return Response({
'results': [
{
'id': pay.id,
'amount': str(pay.amount),
'currency': pay.currency,
'payment_method': pay.payment_method,
'status': pay.status,
'created_at': pay.created_at.isoformat(),
'processed_at': pay.processed_at.isoformat() if pay.processed_at else None,
'invoice_id': pay.invoice_id,
'invoice_number': pay.invoice.invoice_number if pay.invoice else None,
'transaction_reference': pay.transaction_reference,
'failure_reason': pay.failure_reason
}
for pay in payments
],
'count': len(payments)
})
@action(detail=False, methods=['get'])
def transactions(self, request):
"""List credit transactions"""
def available_methods(self, request):
"""Get available payment methods for current account"""
account = request.user.account
methods = PaymentService.get_available_payment_methods(account)
return Response(methods)
@action(detail=False, methods=['post'])
def create_manual_payment(self, request):
"""Submit manual payment for approval"""
account = request.user.account
invoice_id = request.data.get('invoice_id')
payment_method = request.data.get('payment_method') # 'bank_transfer' or 'local_wallet'
transaction_reference = request.data.get('transaction_reference')
notes = request.data.get('notes')
if not all([invoice_id, payment_method, transaction_reference]):
return Response(
{'error': 'Missing required fields'},
status=status.HTTP_400_BAD_REQUEST
)
invoice = get_object_or_404(Invoice, id=invoice_id, account=account)
if invoice.status == 'paid':
return Response(
{'error': 'Invoice already paid'},
status=status.HTTP_400_BAD_REQUEST
)
payment = PaymentService.create_manual_payment(
invoice=invoice,
payment_method=payment_method,
transaction_reference=transaction_reference,
admin_notes=notes
)
return Response({
'results': [],
'count': 0
'id': payment.id,
'status': payment.status,
'message': 'Payment submitted for approval. You will be notified once it is reviewed.'
}, status=status.HTTP_201_CREATED)
class CreditPackageViewSet(viewsets.ViewSet):
"""Credit package endpoints"""
permission_classes = [IsAuthenticated]
def list(self, request):
"""List available credit packages"""
packages = CreditPackage.objects.filter(is_active=True).order_by('price')
return Response({
'results': [
{
'id': pkg.id,
'name': pkg.name,
'slug': pkg.slug,
'credits': pkg.credits,
'price': str(pkg.price),
'discount_percentage': pkg.discount_percentage,
'is_featured': pkg.is_featured,
'description': pkg.description,
'display_order': pkg.sort_order
}
for pkg in packages
],
'count': packages.count()
})
@action(detail=True, methods=['post'])
def purchase(self, request, pk=None):
"""Purchase a credit package"""
account = request.user.account
package = get_object_or_404(CreditPackage, id=pk, is_active=True)
payment_method = request.data.get('payment_method', 'stripe')
# Create invoice for credit package
invoice = InvoiceService.create_credit_package_invoice(
account=account,
credit_package=package
)
# Store credit package info in metadata
metadata = {
'credit_package_id': package.id,
'credit_amount': package.credits
}
if payment_method == 'stripe':
# TODO: Create Stripe payment intent
return Response({
'invoice_id': invoice.id,
'message': 'Stripe integration pending',
'next_action': 'redirect_to_stripe_checkout'
})
elif payment_method == 'paypal':
# TODO: Create PayPal order
return Response({
'invoice_id': invoice.id,
'message': 'PayPal integration pending',
'next_action': 'redirect_to_paypal_checkout'
})
else:
# Manual payment
return Response({
'invoice_id': invoice.id,
'invoice_number': invoice.invoice_number,
'total_amount': str(invoice.total_amount),
'message': 'Invoice created. Please submit payment details.',
'next_action': 'submit_manual_payment'
})
class CreditTransactionViewSet(viewsets.ViewSet):
"""Credit transaction history"""
permission_classes = [IsAuthenticated]
def list(self, request):
"""List credit transactions for current account"""
account = request.user.account
transactions = CreditTransaction.objects.filter(
account=account
).order_by('-created_at')[:100]
return Response({
'results': [
{
'id': txn.id,
'amount': txn.amount,
'transaction_type': txn.transaction_type,
'description': txn.description,
'created_at': txn.created_at.isoformat(),
'reference_id': txn.reference_id,
'metadata': txn.metadata
}
for txn in transactions
],
'count': transactions.count(),
'current_balance': account.credit_balance
})
@action(detail=False, methods=['get'])
def usage(self, request):
"""List credit usage"""
def balance(self, request):
"""Get current credit balance"""
account = request.user.account
# Get subscription details
active_subscription = account.subscriptions.filter(status='active').first()
return Response({
'results': [],
'count': 0
'balance': account.credit_balance,
'subscription_plan': active_subscription.plan.name if active_subscription else 'None',
'monthly_credits': active_subscription.plan.monthly_credits if active_subscription else 0,
'subscription_status': active_subscription.status if active_subscription else None
})
class AdminBillingViewSet(viewsets.ViewSet):
"""Admin billing endpoints"""
"""Admin billing management"""
permission_classes = [IsAuthenticated]
@action(detail=False, methods=['get'])
def pending_payments(self, request):
"""List payments pending approval"""
# Check admin permission
if not request.user.is_staff:
return Response(
{'error': 'Admin access required'},
status=status.HTTP_403_FORBIDDEN
)
payments = PaymentService.get_pending_approvals()
return Response({
'results': [
{
'id': pay.id,
'account_name': pay.account.name,
'amount': str(pay.amount),
'currency': pay.currency,
'payment_method': pay.payment_method,
'transaction_reference': pay.transaction_reference,
'created_at': pay.created_at.isoformat(),
'invoice_number': pay.invoice.invoice_number if pay.invoice else None,
'admin_notes': pay.admin_notes
}
for pay in payments
],
'count': len(payments)
})
@action(detail=True, methods=['post'])
def approve_payment(self, request, pk=None):
"""Approve a manual payment"""
if not request.user.is_staff:
return Response(
{'error': 'Admin access required'},
status=status.HTTP_403_FORBIDDEN
)
payment = get_object_or_404(Payment, id=pk)
admin_notes = request.data.get('notes')
try:
payment = PaymentService.approve_manual_payment(
payment=payment,
approved_by_user_id=request.user.id,
admin_notes=admin_notes
)
return Response({
'id': payment.id,
'status': payment.status,
'message': 'Payment approved successfully'
})
except ValueError as e:
return Response(
{'error': str(e)},
status=status.HTTP_400_BAD_REQUEST
)
@action(detail=True, methods=['post'])
def reject_payment(self, request, pk=None):
"""Reject a manual payment"""
if not request.user.is_staff:
return Response(
{'error': 'Admin access required'},
status=status.HTTP_403_FORBIDDEN
)
payment = get_object_or_404(Payment, id=pk)
rejection_reason = request.data.get('reason', 'No reason provided')
try:
payment = PaymentService.reject_manual_payment(
payment=payment,
rejected_by_user_id=request.user.id,
rejection_reason=rejection_reason
)
return Response({
'id': payment.id,
'status': payment.status,
'message': 'Payment rejected'
})
except ValueError as e:
return Response(
{'error': str(e)},
status=status.HTTP_400_BAD_REQUEST
)
@action(detail=False, methods=['get'])
def stats(self, request):
"""System billing stats"""
if not request.user.is_staff:
return Response(
{'error': 'Admin access required'},
status=status.HTTP_403_FORBIDDEN
)
from django.db.models import Sum, Count
from ...auth.models import Account
total_accounts = Account.objects.count()
active_subscriptions = Account.objects.filter(
subscriptions__status='active'
).distinct().count()
total_revenue = Payment.objects.filter(
status='completed',
amount__gt=0
).aggregate(total=Sum('amount'))['total'] or 0
pending_approvals = Payment.objects.filter(
status='pending_approval'
).count()
return Response({
'total_users': 0,
'active_users': 0,
'total_credits_issued': 0,
'total_credits_used': 0
'total_accounts': total_accounts,
'active_subscriptions': active_subscriptions,
'total_revenue': str(total_revenue),
'pending_approvals': pending_approvals,
'invoices_pending': Invoice.objects.filter(status='pending').count(),
'invoices_paid': Invoice.objects.filter(status='paid').count()
})

View File

@@ -0,0 +1,152 @@
# Generated by Django 5.2.8 on 2025-12-04 23:35
import django.core.validators
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('billing', '0003_creditcostconfig'),
('igny8_core_auth', '0003_add_sync_event_model'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='CreditPackage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('slug', models.SlugField(unique=True)),
('credits', models.IntegerField(validators=[django.core.validators.MinValueValidator(1)])),
('price', models.DecimalField(decimal_places=2, max_digits=10)),
('discount_percentage', models.IntegerField(default=0, help_text='Discount percentage (0-100)')),
('stripe_product_id', models.CharField(blank=True, max_length=255, null=True)),
('stripe_price_id', models.CharField(blank=True, max_length=255, null=True)),
('paypal_plan_id', models.CharField(blank=True, max_length=255, null=True)),
('is_active', models.BooleanField(db_index=True, default=True)),
('is_featured', models.BooleanField(default=False, help_text='Show as featured package')),
('description', models.TextField(blank=True)),
('features', models.JSONField(default=list, help_text='Bonus features or highlights')),
('sort_order', models.IntegerField(default=0, help_text='Display order (lower = first)')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'db_table': 'igny8_credit_packages',
'ordering': ['sort_order', 'price'],
},
),
migrations.CreateModel(
name='Invoice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('invoice_number', models.CharField(db_index=True, max_length=50, unique=True)),
('subtotal', models.DecimalField(decimal_places=2, max_digits=10)),
('tax', models.DecimalField(decimal_places=2, default=0, max_digits=10)),
('total', models.DecimalField(decimal_places=2, max_digits=10)),
('status', models.CharField(choices=[('draft', 'Draft'), ('pending', 'Pending'), ('paid', 'Paid'), ('void', 'Void'), ('uncollectible', 'Uncollectible')], db_index=True, default='pending', max_length=20)),
('invoice_date', models.DateField(db_index=True)),
('due_date', models.DateField()),
('paid_at', models.DateTimeField(blank=True, null=True)),
('line_items', models.JSONField(default=list, help_text='Invoice line items: [{description, amount, quantity}]')),
('stripe_invoice_id', models.CharField(blank=True, max_length=255, null=True)),
('payment_method', models.CharField(blank=True, max_length=50, null=True)),
('notes', models.TextField(blank=True)),
('metadata', models.JSONField(default=dict)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.account')),
('subscription', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoices', to='igny8_core_auth.subscription')),
],
options={
'db_table': 'igny8_invoices',
'ordering': ['-invoice_date', '-created_at'],
},
),
migrations.CreateModel(
name='Payment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('amount', models.DecimalField(decimal_places=2, max_digits=10)),
('currency', models.CharField(default='USD', max_length=3)),
('status', models.CharField(choices=[('pending', 'Pending'), ('processing', 'Processing'), ('succeeded', 'Succeeded'), ('failed', 'Failed'), ('refunded', 'Refunded'), ('cancelled', 'Cancelled')], db_index=True, default='pending', max_length=20)),
('payment_method', models.CharField(choices=[('stripe', 'Stripe (Credit/Debit Card)'), ('paypal', 'PayPal'), ('bank_transfer', 'Bank Transfer (Manual)'), ('local_wallet', 'Local Wallet (Manual)'), ('manual', 'Manual Payment')], db_index=True, max_length=50)),
('stripe_payment_intent_id', models.CharField(blank=True, max_length=255, null=True)),
('stripe_charge_id', models.CharField(blank=True, max_length=255, null=True)),
('paypal_order_id', models.CharField(blank=True, max_length=255, null=True)),
('paypal_capture_id', models.CharField(blank=True, max_length=255, null=True)),
('manual_reference', models.CharField(blank=True, help_text='Bank transfer reference, wallet transaction ID, etc.', max_length=255)),
('manual_notes', models.TextField(blank=True, help_text='Admin notes for manual payments')),
('approved_at', models.DateTimeField(blank=True, null=True)),
('processed_at', models.DateTimeField(blank=True, null=True)),
('failed_at', models.DateTimeField(blank=True, null=True)),
('refunded_at', models.DateTimeField(blank=True, null=True)),
('failure_reason', models.TextField(blank=True)),
('metadata', models.JSONField(default=dict)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('account', models.ForeignKey(db_column='tenant_id', on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_set', to='igny8_core_auth.account')),
('approved_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='approved_payments', to=settings.AUTH_USER_MODEL)),
('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='billing.invoice')),
],
options={
'db_table': 'igny8_payments',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='PaymentMethodConfig',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('country_code', models.CharField(db_index=True, help_text='ISO 2-letter country code (e.g., US, GB, IN)', max_length=2)),
('payment_method', models.CharField(choices=[('stripe', 'Stripe'), ('paypal', 'PayPal'), ('bank_transfer', 'Bank Transfer'), ('local_wallet', 'Local Wallet')], max_length=50)),
('is_enabled', models.BooleanField(default=True)),
('display_name', models.CharField(blank=True, max_length=100)),
('instructions', models.TextField(blank=True, help_text='Payment instructions for users')),
('bank_name', models.CharField(blank=True, max_length=255)),
('account_number', models.CharField(blank=True, max_length=255)),
('routing_number', models.CharField(blank=True, max_length=255)),
('swift_code', models.CharField(blank=True, max_length=255)),
('wallet_type', models.CharField(blank=True, help_text='E.g., PayTM, PhonePe, etc.', max_length=100)),
('wallet_id', models.CharField(blank=True, max_length=255)),
('sort_order', models.IntegerField(default=0)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'Payment Method Configuration',
'verbose_name_plural': 'Payment Method Configurations',
'db_table': 'igny8_payment_method_config',
'ordering': ['country_code', 'sort_order'],
'unique_together': {('country_code', 'payment_method')},
},
),
migrations.AddIndex(
model_name='invoice',
index=models.Index(fields=['account', 'status'], name='igny8_invoi_tenant__4c2de3_idx'),
),
migrations.AddIndex(
model_name='invoice',
index=models.Index(fields=['account', 'invoice_date'], name='igny8_invoi_tenant__5107b7_idx'),
),
migrations.AddIndex(
model_name='invoice',
index=models.Index(fields=['invoice_number'], name='igny8_invoi_invoice_6f16b5_idx'),
),
migrations.AddIndex(
model_name='payment',
index=models.Index(fields=['account', 'status'], name='igny8_payme_tenant__62289b_idx'),
),
migrations.AddIndex(
model_name='payment',
index=models.Index(fields=['account', 'payment_method'], name='igny8_payme_tenant__7d34bb_idx'),
),
migrations.AddIndex(
model_name='payment',
index=models.Index(fields=['invoice', 'status'], name='igny8_payme_invoice_316f1c_idx'),
),
]

View File

@@ -40,7 +40,8 @@ urlpatterns = [
path('api/v1/planner/', include('igny8_core.modules.planner.urls')),
path('api/v1/writer/', include('igny8_core.modules.writer.urls')),
path('api/v1/system/', include('igny8_core.modules.system.urls')),
path('api/v1/billing/', include('igny8_core.modules.billing.urls')), # Billing endpoints
path('api/v1/billing/', include('igny8_core.modules.billing.urls')), # Billing endpoints (legacy)
path('api/v1/billing/v2/', include('igny8_core.business.billing.urls')), # New billing endpoints (invoices, payments)
path('api/v1/admin/', include('igny8_core.modules.billing.admin_urls')), # Admin billing
path('api/v1/automation/', include('igny8_core.business.automation.urls')), # Automation endpoints
path('api/v1/linker/', include('igny8_core.modules.linker.urls')), # Linker endpoints

View File

@@ -0,0 +1,75 @@
"""
Seed credit packages for testing
"""
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'igny8_core.settings')
django.setup()
from igny8_core.business.billing.models import CreditPackage
from decimal import Decimal
def seed_credit_packages():
"""Create default credit packages"""
packages = [
{
'name': 'Starter Pack',
'slug': 'starter-pack',
'credits': 1000,
'price': Decimal('9.99'),
'discount_percentage': 0,
'description': 'Perfect for trying out the platform',
'sort_order': 1,
'is_featured': False
},
{
'name': 'Professional Pack',
'slug': 'professional-pack',
'credits': 5000,
'price': Decimal('39.99'),
'discount_percentage': 20,
'description': 'Best for growing teams',
'sort_order': 2,
'is_featured': True
},
{
'name': 'Business Pack',
'slug': 'business-pack',
'credits': 15000,
'price': Decimal('99.99'),
'discount_percentage': 30,
'description': 'Ideal for established businesses',
'sort_order': 3,
'is_featured': False
},
{
'name': 'Enterprise Pack',
'slug': 'enterprise-pack',
'credits': 50000,
'price': Decimal('299.99'),
'discount_percentage': 40,
'description': 'Maximum value for high-volume users',
'sort_order': 4,
'is_featured': True
}
]
created_count = 0
for pkg_data in packages:
pkg, created = CreditPackage.objects.get_or_create(
slug=pkg_data['slug'],
defaults=pkg_data
)
if created:
created_count += 1
print(f"✅ Created: {pkg.name} - {pkg.credits:,} credits for ${pkg.price}")
else:
print(f"⏭️ Exists: {pkg.name}")
print(f"\n✅ Seeded {created_count} new credit packages")
print(f"📊 Total active packages: {CreditPackage.objects.filter(is_active=True).count()}")
if __name__ == '__main__':
seed_credit_packages()

View File

@@ -0,0 +1,125 @@
"""
Seed payment method configurations
"""
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'igny8_core.settings')
django.setup()
from igny8_core.business.billing.models import PaymentMethodConfig
def seed_payment_configs():
"""Create payment method configurations for various countries"""
configs = [
# United States - Stripe and PayPal only
{
'country_code': 'US',
'payment_method': 'stripe',
'is_enabled': True,
'display_name': 'Credit/Debit Card',
'instructions': 'Pay securely with your credit or debit card via Stripe',
'sort_order': 1
},
{
'country_code': 'US',
'payment_method': 'paypal',
'is_enabled': True,
'display_name': 'PayPal',
'instructions': 'Pay with your PayPal account',
'sort_order': 2
},
# India - All methods including manual
{
'country_code': 'IN',
'payment_method': 'stripe',
'is_enabled': True,
'display_name': 'Credit/Debit Card',
'instructions': 'Pay securely with your credit or debit card',
'sort_order': 1
},
{
'country_code': 'IN',
'payment_method': 'paypal',
'is_enabled': True,
'display_name': 'PayPal',
'instructions': 'Pay with your PayPal account',
'sort_order': 2
},
{
'country_code': 'IN',
'payment_method': 'bank_transfer',
'is_enabled': True,
'display_name': 'Bank Transfer (NEFT/IMPS/RTGS)',
'instructions': 'Transfer funds to our bank account. Payment will be verified within 1-2 business days.',
'bank_name': 'HDFC Bank',
'account_number': 'XXXXXXXXXXXXX',
'routing_number': 'HDFC0000XXX',
'swift_code': 'HDFCINBB',
'sort_order': 3
},
{
'country_code': 'IN',
'payment_method': 'local_wallet',
'is_enabled': True,
'display_name': 'UPI / Digital Wallet',
'instructions': 'Pay via Paytm, PhonePe, Google Pay, or other UPI apps. Upload payment screenshot for verification.',
'wallet_type': 'UPI',
'wallet_id': 'igny8@paytm',
'sort_order': 4
},
# United Kingdom - Stripe, PayPal, Bank Transfer
{
'country_code': 'GB',
'payment_method': 'stripe',
'is_enabled': True,
'display_name': 'Credit/Debit Card',
'instructions': 'Pay securely with your credit or debit card',
'sort_order': 1
},
{
'country_code': 'GB',
'payment_method': 'paypal',
'is_enabled': True,
'display_name': 'PayPal',
'instructions': 'Pay with your PayPal account',
'sort_order': 2
},
{
'country_code': 'GB',
'payment_method': 'bank_transfer',
'is_enabled': True,
'display_name': 'Bank Transfer (BACS/Faster Payments)',
'instructions': 'Transfer funds to our UK bank account.',
'bank_name': 'Barclays Bank',
'account_number': 'XXXXXXXX',
'routing_number': 'XX-XX-XX',
'swift_code': 'BARCGB22',
'sort_order': 3
},
]
created_count = 0
updated_count = 0
for config_data in configs:
config, created = PaymentMethodConfig.objects.update_or_create(
country_code=config_data['country_code'],
payment_method=config_data['payment_method'],
defaults={k: v for k, v in config_data.items() if k not in ['country_code', 'payment_method']}
)
if created:
created_count += 1
print(f"✅ Created: {config.country_code} - {config.get_payment_method_display()}")
else:
updated_count += 1
print(f"🔄 Updated: {config.country_code} - {config.get_payment_method_display()}")
print(f"\n✅ Created {created_count} configurations")
print(f"🔄 Updated {updated_count} configurations")
print(f"📊 Total active: {PaymentMethodConfig.objects.filter(is_enabled=True).count()}")
if __name__ == '__main__':
seed_payment_configs()