docs and billing adn acaoutn 40%
This commit is contained in:
@@ -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()}"
|
||||
|
||||
249
backend/igny8_core/business/billing/services/invoice_service.py
Normal file
249
backend/igny8_core/business/billing/services/invoice_service.py
Normal 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')
|
||||
)
|
||||
375
backend/igny8_core/business/billing/services/payment_service.py
Normal file
375
backend/igny8_core/business/billing/services/payment_service.py
Normal 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]
|
||||
)
|
||||
23
backend/igny8_core/business/billing/urls.py
Normal file
23
backend/igny8_core/business/billing/urls.py
Normal 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)),
|
||||
]
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user