billing admin account 1
This commit is contained in:
@@ -222,7 +222,7 @@ class UsageAnalyticsViewSet(viewsets.ViewSet):
|
|||||||
'period_days': days,
|
'period_days': days,
|
||||||
'start_date': start_date.isoformat(),
|
'start_date': start_date.isoformat(),
|
||||||
'end_date': timezone.now().isoformat(),
|
'end_date': timezone.now().isoformat(),
|
||||||
'current_balance': account.credit_balance,
|
'current_balance': account.credits,
|
||||||
'usage_by_type': list(usage_by_type),
|
'usage_by_type': list(usage_by_type),
|
||||||
'purchases_by_type': list(purchases_by_type),
|
'purchases_by_type': list(purchases_by_type),
|
||||||
'daily_usage': daily_usage,
|
'daily_usage': daily_usage,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Billing Models for Credit System
|
Billing Models for Credit System
|
||||||
"""
|
"""
|
||||||
|
from decimal import Decimal
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -22,6 +23,11 @@ class CreditTransaction(AccountBaseModel):
|
|||||||
balance_after = models.IntegerField(help_text="Credit balance after this transaction")
|
balance_after = models.IntegerField(help_text="Credit balance after this transaction")
|
||||||
description = models.CharField(max_length=255)
|
description = models.CharField(max_length=255)
|
||||||
metadata = models.JSONField(default=dict, help_text="Additional context (AI call details, etc.)")
|
metadata = models.JSONField(default=dict, help_text="Additional context (AI call details, etc.)")
|
||||||
|
reference_id = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
blank=True,
|
||||||
|
help_text="Optional reference (e.g., payment id, invoice id)"
|
||||||
|
)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -183,9 +189,10 @@ class Invoice(AccountBaseModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Amounts
|
# Amounts
|
||||||
subtotal = models.DecimalField(max_digits=10, decimal_places=2)
|
subtotal = models.DecimalField(max_digits=10, decimal_places=2, default=0)
|
||||||
tax = models.DecimalField(max_digits=10, decimal_places=2, default=0)
|
tax = models.DecimalField(max_digits=10, decimal_places=2, default=0)
|
||||||
total = models.DecimalField(max_digits=10, decimal_places=2)
|
total = models.DecimalField(max_digits=10, decimal_places=2, default=0)
|
||||||
|
currency = models.CharField(max_length=3, default='USD')
|
||||||
|
|
||||||
# Status
|
# Status
|
||||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', db_index=True)
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', db_index=True)
|
||||||
@@ -201,6 +208,9 @@ class Invoice(AccountBaseModel):
|
|||||||
# Payment integration
|
# Payment integration
|
||||||
stripe_invoice_id = models.CharField(max_length=255, null=True, blank=True)
|
stripe_invoice_id = models.CharField(max_length=255, null=True, blank=True)
|
||||||
payment_method = models.CharField(max_length=50, null=True, blank=True)
|
payment_method = models.CharField(max_length=50, null=True, blank=True)
|
||||||
|
billing_email = models.EmailField(null=True, blank=True)
|
||||||
|
billing_period_start = models.DateTimeField(null=True, blank=True)
|
||||||
|
billing_period_end = models.DateTimeField(null=True, blank=True)
|
||||||
|
|
||||||
# Metadata
|
# Metadata
|
||||||
notes = models.TextField(blank=True)
|
notes = models.TextField(blank=True)
|
||||||
@@ -222,6 +232,45 @@ class Invoice(AccountBaseModel):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Invoice {self.invoice_number} - {self.account.name if self.account else 'No Account'}"
|
return f"Invoice {self.invoice_number} - {self.account.name if self.account else 'No Account'}"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Helpers to keep service code working with legacy field names
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@property
|
||||||
|
def subtotal_amount(self):
|
||||||
|
return self.subtotal
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tax_amount(self):
|
||||||
|
return self.tax
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_amount(self):
|
||||||
|
return self.total
|
||||||
|
|
||||||
|
def add_line_item(self, description: str, quantity: int, unit_price: Decimal, amount: Decimal = None):
|
||||||
|
"""Append a line item and keep JSON shape consistent."""
|
||||||
|
items = list(self.line_items or [])
|
||||||
|
qty = quantity or 1
|
||||||
|
amt = Decimal(amount) if amount is not None else Decimal(unit_price) * qty
|
||||||
|
items.append({
|
||||||
|
'description': description,
|
||||||
|
'quantity': qty,
|
||||||
|
'unit_price': str(unit_price),
|
||||||
|
'amount': str(amt),
|
||||||
|
})
|
||||||
|
self.line_items = items
|
||||||
|
|
||||||
|
def calculate_totals(self):
|
||||||
|
"""Recompute subtotal, tax, and total from line_items."""
|
||||||
|
subtotal = Decimal('0')
|
||||||
|
for item in self.line_items or []:
|
||||||
|
try:
|
||||||
|
subtotal += Decimal(str(item.get('amount') or 0))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.subtotal = subtotal
|
||||||
|
self.total = subtotal + (self.tax or Decimal('0'))
|
||||||
|
|
||||||
|
|
||||||
class Payment(AccountBaseModel):
|
class Payment(AccountBaseModel):
|
||||||
"""
|
"""
|
||||||
@@ -230,8 +279,10 @@ class Payment(AccountBaseModel):
|
|||||||
"""
|
"""
|
||||||
STATUS_CHOICES = [
|
STATUS_CHOICES = [
|
||||||
('pending', 'Pending'),
|
('pending', 'Pending'),
|
||||||
|
('pending_approval', 'Pending Approval'),
|
||||||
('processing', 'Processing'),
|
('processing', 'Processing'),
|
||||||
('succeeded', 'Succeeded'),
|
('succeeded', 'Succeeded'),
|
||||||
|
('completed', 'Completed'), # Legacy alias for succeeded
|
||||||
('failed', 'Failed'),
|
('failed', 'Failed'),
|
||||||
('refunded', 'Refunded'),
|
('refunded', 'Refunded'),
|
||||||
('cancelled', 'Cancelled'),
|
('cancelled', 'Cancelled'),
|
||||||
@@ -272,6 +323,8 @@ class Payment(AccountBaseModel):
|
|||||||
help_text="Bank transfer reference, wallet transaction ID, etc."
|
help_text="Bank transfer reference, wallet transaction ID, etc."
|
||||||
)
|
)
|
||||||
manual_notes = models.TextField(blank=True, help_text="Admin notes for manual payments")
|
manual_notes = models.TextField(blank=True, help_text="Admin notes for manual payments")
|
||||||
|
transaction_reference = models.CharField(max_length=255, blank=True)
|
||||||
|
admin_notes = models.TextField(blank=True, help_text="Internal notes on approval/rejection")
|
||||||
approved_by = models.ForeignKey(
|
approved_by = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL,
|
||||||
null=True,
|
null=True,
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ class InvoiceService:
|
|||||||
billing_email=account.billing_email or account.users.filter(role='owner').first().email,
|
billing_email=account.billing_email or account.users.filter(role='owner').first().email,
|
||||||
status='pending',
|
status='pending',
|
||||||
currency='USD',
|
currency='USD',
|
||||||
|
invoice_date=timezone.now().date(),
|
||||||
|
due_date=billing_period_end.date(),
|
||||||
billing_period_start=billing_period_start,
|
billing_period_start=billing_period_start,
|
||||||
billing_period_end=billing_period_end
|
billing_period_end=billing_period_end
|
||||||
)
|
)
|
||||||
@@ -83,7 +85,13 @@ class InvoiceService:
|
|||||||
invoice_number=InvoiceService.generate_invoice_number(account),
|
invoice_number=InvoiceService.generate_invoice_number(account),
|
||||||
billing_email=account.billing_email or account.users.filter(role='owner').first().email,
|
billing_email=account.billing_email or account.users.filter(role='owner').first().email,
|
||||||
status='pending',
|
status='pending',
|
||||||
currency='USD'
|
currency='USD',
|
||||||
|
invoice_date=timezone.now().date(),
|
||||||
|
due_date=timezone.now().date(),
|
||||||
|
metadata={
|
||||||
|
'credit_package_id': credit_package.id,
|
||||||
|
'credit_amount': credit_package.credits,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add line item for credit package
|
# Add line item for credit package
|
||||||
@@ -125,6 +133,7 @@ class InvoiceService:
|
|||||||
status='draft',
|
status='draft',
|
||||||
currency='USD',
|
currency='USD',
|
||||||
notes=notes,
|
notes=notes,
|
||||||
|
invoice_date=timezone.now().date(),
|
||||||
due_date=due_date or (timezone.now() + timedelta(days=30))
|
due_date=due_date or (timezone.now() + timedelta(days=30))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,12 @@ class PaymentService:
|
|||||||
if payment_method not in ['bank_transfer', 'local_wallet', 'manual']:
|
if payment_method not in ['bank_transfer', 'local_wallet', 'manual']:
|
||||||
raise ValueError("Invalid manual payment method")
|
raise ValueError("Invalid manual payment method")
|
||||||
|
|
||||||
|
meta = metadata or {}
|
||||||
|
# propagate credit package metadata from invoice if present
|
||||||
|
if invoice.metadata.get('credit_package_id'):
|
||||||
|
meta.setdefault('credit_package_id', invoice.metadata.get('credit_package_id'))
|
||||||
|
meta.setdefault('credit_amount', invoice.metadata.get('credit_amount'))
|
||||||
|
|
||||||
payment = Payment.objects.create(
|
payment = Payment.objects.create(
|
||||||
account=invoice.account,
|
account=invoice.account,
|
||||||
invoice=invoice,
|
invoice=invoice,
|
||||||
@@ -86,7 +92,7 @@ class PaymentService:
|
|||||||
status='pending_approval',
|
status='pending_approval',
|
||||||
transaction_reference=transaction_reference,
|
transaction_reference=transaction_reference,
|
||||||
admin_notes=admin_notes,
|
admin_notes=admin_notes,
|
||||||
metadata=metadata or {}
|
metadata=meta
|
||||||
)
|
)
|
||||||
|
|
||||||
return payment
|
return payment
|
||||||
@@ -102,7 +108,7 @@ class PaymentService:
|
|||||||
"""
|
"""
|
||||||
from .invoice_service import InvoiceService
|
from .invoice_service import InvoiceService
|
||||||
|
|
||||||
payment.status = 'completed'
|
payment.status = 'succeeded'
|
||||||
payment.processed_at = timezone.now()
|
payment.processed_at = timezone.now()
|
||||||
|
|
||||||
if transaction_id:
|
if transaction_id:
|
||||||
@@ -153,9 +159,10 @@ class PaymentService:
|
|||||||
if payment.status != 'pending_approval':
|
if payment.status != 'pending_approval':
|
||||||
raise ValueError("Payment is not pending approval")
|
raise ValueError("Payment is not pending approval")
|
||||||
|
|
||||||
payment.status = 'completed'
|
payment.status = 'succeeded'
|
||||||
payment.processed_at = timezone.now()
|
payment.processed_at = timezone.now()
|
||||||
payment.approved_by_id = approved_by_user_id
|
payment.approved_by_id = approved_by_user_id
|
||||||
|
payment.approved_at = timezone.now()
|
||||||
|
|
||||||
if admin_notes:
|
if admin_notes:
|
||||||
payment.admin_notes = f"{payment.admin_notes}\n\nApproval notes: {admin_notes}" if payment.admin_notes else admin_notes
|
payment.admin_notes = f"{payment.admin_notes}\n\nApproval notes: {admin_notes}" if payment.admin_notes else admin_notes
|
||||||
@@ -212,6 +219,11 @@ class PaymentService:
|
|||||||
except CreditPackage.DoesNotExist:
|
except CreditPackage.DoesNotExist:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Update account balance
|
||||||
|
account: Account = payment.account
|
||||||
|
account.credits = (account.credits or 0) + credit_package.credits
|
||||||
|
account.save(update_fields=['credits', 'updated_at'])
|
||||||
|
|
||||||
# Create credit transaction
|
# Create credit transaction
|
||||||
CreditTransaction.objects.create(
|
CreditTransaction.objects.create(
|
||||||
account=payment.account,
|
account=payment.account,
|
||||||
@@ -244,14 +256,20 @@ class PaymentService:
|
|||||||
return {
|
return {
|
||||||
'methods': [
|
'methods': [
|
||||||
{
|
{
|
||||||
|
'id': 'stripe-default',
|
||||||
'type': 'stripe',
|
'type': 'stripe',
|
||||||
'name': 'Credit/Debit Card',
|
'name': 'Credit/Debit Card',
|
||||||
'instructions': 'Pay securely with your credit or debit card'
|
'display_name': 'Credit/Debit Card',
|
||||||
|
'instructions': 'Pay securely with your credit or debit card',
|
||||||
|
'is_enabled': True,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
'id': 'paypal-default',
|
||||||
'type': 'paypal',
|
'type': 'paypal',
|
||||||
'name': 'PayPal',
|
'name': 'PayPal',
|
||||||
'instructions': 'Pay with your PayPal account'
|
'display_name': 'PayPal',
|
||||||
|
'instructions': 'Pay with your PayPal account',
|
||||||
|
'is_enabled': True,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
'stripe': True,
|
'stripe': True,
|
||||||
@@ -272,9 +290,12 @@ class PaymentService:
|
|||||||
for config in configs:
|
for config in configs:
|
||||||
method_flags[config.payment_method] = True
|
method_flags[config.payment_method] = True
|
||||||
method_data = {
|
method_data = {
|
||||||
|
'id': f"{config.country_code}-{config.payment_method}-{config.id}",
|
||||||
'type': config.payment_method,
|
'type': config.payment_method,
|
||||||
'name': config.display_name or config.get_payment_method_display(),
|
'name': config.display_name or config.get_payment_method_display(),
|
||||||
'instructions': config.instructions
|
'display_name': config.display_name or config.get_payment_method_display(),
|
||||||
|
'instructions': config.instructions,
|
||||||
|
'is_enabled': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add bank details if bank_transfer
|
# Add bank details if bank_transfer
|
||||||
@@ -323,8 +344,8 @@ class PaymentService:
|
|||||||
TODO: Implement actual refund logic for Stripe/PayPal
|
TODO: Implement actual refund logic for Stripe/PayPal
|
||||||
For now, just mark as refunded
|
For now, just mark as refunded
|
||||||
"""
|
"""
|
||||||
if payment.status != 'completed':
|
if payment.status not in ['completed', 'succeeded']:
|
||||||
raise ValueError("Can only refund completed payments")
|
raise ValueError("Can only refund succeeded/complete payments")
|
||||||
|
|
||||||
refund_amount = amount or payment.amount
|
refund_amount = amount or payment.amount
|
||||||
|
|
||||||
@@ -338,8 +359,9 @@ class PaymentService:
|
|||||||
amount=-refund_amount, # Negative amount for refund
|
amount=-refund_amount, # Negative amount for refund
|
||||||
currency=payment.currency,
|
currency=payment.currency,
|
||||||
payment_method=payment.payment_method,
|
payment_method=payment.payment_method,
|
||||||
status='completed',
|
status='refunded',
|
||||||
processed_at=timezone.now(),
|
processed_at=timezone.now(),
|
||||||
|
refunded_at=timezone.now(),
|
||||||
metadata={
|
metadata={
|
||||||
'refund_for_payment_id': payment.id,
|
'refund_for_payment_id': payment.id,
|
||||||
'refund_reason': reason,
|
'refund_reason': reason,
|
||||||
@@ -348,10 +370,16 @@ class PaymentService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Update original payment metadata
|
# Update original payment metadata
|
||||||
payment.metadata['refunded'] = True
|
meta = payment.metadata or {}
|
||||||
payment.metadata['refund_payment_id'] = refund.id
|
meta['refunded'] = True
|
||||||
payment.metadata['refund_amount'] = str(refund_amount)
|
meta['refund_payment_id'] = refund.id
|
||||||
payment.save()
|
meta['refund_amount'] = str(refund_amount)
|
||||||
|
if reason:
|
||||||
|
meta['refund_reason'] = reason
|
||||||
|
payment.metadata = meta
|
||||||
|
payment.status = 'refunded'
|
||||||
|
payment.refunded_at = timezone.now()
|
||||||
|
payment.save(update_fields=['metadata', 'status', 'refunded_at', 'updated_at'])
|
||||||
|
|
||||||
return refund
|
return refund
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from rest_framework.response import Response
|
|||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
from .models import Invoice, Payment, CreditPackage, PaymentMethodConfig, CreditTransaction
|
from .models import Invoice, Payment, CreditPackage, PaymentMethodConfig, CreditTransaction
|
||||||
from .services.invoice_service import InvoiceService
|
from .services.invoice_service import InvoiceService
|
||||||
@@ -126,16 +127,21 @@ class PaymentViewSet(viewsets.ViewSet):
|
|||||||
"""Get available payment methods for current account"""
|
"""Get available payment methods for current account"""
|
||||||
account = request.user.account
|
account = request.user.account
|
||||||
methods = PaymentService.get_available_payment_methods(account)
|
methods = PaymentService.get_available_payment_methods(account)
|
||||||
|
method_list = methods.pop('methods', [])
|
||||||
|
|
||||||
return Response(methods)
|
return Response({
|
||||||
|
'results': method_list,
|
||||||
|
'count': len(method_list),
|
||||||
|
**methods
|
||||||
|
})
|
||||||
|
|
||||||
@action(detail=False, methods=['post'])
|
@action(detail=False, methods=['post'], url_path='manual')
|
||||||
def create_manual_payment(self, request):
|
def create_manual_payment(self, request):
|
||||||
"""Submit manual payment for approval"""
|
"""Submit manual payment for approval"""
|
||||||
account = request.user.account
|
account = request.user.account
|
||||||
invoice_id = request.data.get('invoice_id')
|
invoice_id = request.data.get('invoice_id')
|
||||||
payment_method = request.data.get('payment_method') # 'bank_transfer' or 'local_wallet'
|
payment_method = request.data.get('payment_method') # 'bank_transfer' or 'local_wallet'
|
||||||
transaction_reference = request.data.get('transaction_reference')
|
transaction_reference = request.data.get('transaction_reference') or request.data.get('reference')
|
||||||
notes = request.data.get('notes')
|
notes = request.data.get('notes')
|
||||||
|
|
||||||
if not all([invoice_id, payment_method, transaction_reference]):
|
if not all([invoice_id, payment_method, transaction_reference]):
|
||||||
@@ -261,22 +267,32 @@ class CreditTransactionViewSet(viewsets.ViewSet):
|
|||||||
for txn in transactions
|
for txn in transactions
|
||||||
],
|
],
|
||||||
'count': transactions.count(),
|
'count': transactions.count(),
|
||||||
'current_balance': account.credit_balance
|
'current_balance': account.credits
|
||||||
})
|
})
|
||||||
|
|
||||||
@action(detail=False, methods=['get'])
|
@action(detail=False, methods=['get'])
|
||||||
def balance(self, request):
|
def balance(self, request):
|
||||||
"""Get current credit balance"""
|
"""Get current credit balance"""
|
||||||
account = request.user.account
|
account = request.user.account
|
||||||
|
from django.utils import timezone
|
||||||
# Get subscription details
|
from datetime import timedelta
|
||||||
active_subscription = account.subscriptions.filter(status='active').first()
|
now = timezone.now()
|
||||||
|
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
used_this_month = abs(
|
||||||
|
CreditTransaction.objects.filter(
|
||||||
|
account=account,
|
||||||
|
created_at__gte=month_start,
|
||||||
|
amount__lt=0
|
||||||
|
).aggregate(total=models.Sum('amount'))['total'] or 0
|
||||||
|
)
|
||||||
|
plan = getattr(account, 'plan', None)
|
||||||
|
included = plan.included_credits if plan else 0
|
||||||
|
|
||||||
return Response({
|
return Response({
|
||||||
'balance': account.credit_balance,
|
'credits': account.credits,
|
||||||
'subscription_plan': active_subscription.plan.name if active_subscription else 'None',
|
'plan_credits_per_month': included,
|
||||||
'monthly_credits': active_subscription.plan.monthly_credits if active_subscription else 0,
|
'credits_used_this_month': used_this_month,
|
||||||
'subscription_status': active_subscription.status if active_subscription else None
|
'credits_remaining': max(account.credits, 0),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -284,15 +300,95 @@ class AdminBillingViewSet(viewsets.ViewSet):
|
|||||||
"""Admin billing management"""
|
"""Admin billing management"""
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
@action(detail=False, methods=['get'])
|
def _require_admin(self, request):
|
||||||
def pending_payments(self, request):
|
if not request.user.is_staff and not getattr(request.user, 'is_superuser', False):
|
||||||
"""List payments pending approval"""
|
|
||||||
# Check admin permission
|
|
||||||
if not request.user.is_staff:
|
|
||||||
return Response(
|
return Response(
|
||||||
{'error': 'Admin access required'},
|
{'error': 'Admin access required'},
|
||||||
status=status.HTTP_403_FORBIDDEN
|
status=status.HTTP_403_FORBIDDEN
|
||||||
)
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@action(detail=False, methods=['get'])
|
||||||
|
def invoices(self, request):
|
||||||
|
"""List invoices across all accounts (admin)"""
|
||||||
|
error = self._require_admin(request)
|
||||||
|
if error:
|
||||||
|
return error
|
||||||
|
|
||||||
|
status_filter = request.query_params.get('status')
|
||||||
|
account_id = request.query_params.get('account_id')
|
||||||
|
qs = Invoice.objects.all().select_related('account').order_by('-created_at')
|
||||||
|
if status_filter:
|
||||||
|
qs = qs.filter(status=status_filter)
|
||||||
|
if account_id:
|
||||||
|
qs = qs.filter(account_id=account_id)
|
||||||
|
|
||||||
|
invoices = qs[:200]
|
||||||
|
return Response({
|
||||||
|
'results': [
|
||||||
|
{
|
||||||
|
'id': inv.id,
|
||||||
|
'invoice_number': inv.invoice_number,
|
||||||
|
'status': inv.status,
|
||||||
|
'total_amount': str(getattr(inv, 'total_amount', inv.total)),
|
||||||
|
'subtotal': str(inv.subtotal),
|
||||||
|
'tax_amount': str(getattr(inv, 'tax_amount', inv.tax)),
|
||||||
|
'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,
|
||||||
|
'account_name': inv.account.name if inv.account else None,
|
||||||
|
}
|
||||||
|
for inv in invoices
|
||||||
|
],
|
||||||
|
'count': qs.count(),
|
||||||
|
})
|
||||||
|
|
||||||
|
@action(detail=False, methods=['get'])
|
||||||
|
def payments(self, request):
|
||||||
|
"""List payments across all accounts (admin)"""
|
||||||
|
error = self._require_admin(request)
|
||||||
|
if error:
|
||||||
|
return error
|
||||||
|
|
||||||
|
status_filter = request.query_params.get('status')
|
||||||
|
account_id = request.query_params.get('account_id')
|
||||||
|
payment_method = request.query_params.get('payment_method')
|
||||||
|
qs = Payment.objects.all().select_related('account', 'invoice').order_by('-created_at')
|
||||||
|
if status_filter:
|
||||||
|
qs = qs.filter(status=status_filter)
|
||||||
|
if account_id:
|
||||||
|
qs = qs.filter(account_id=account_id)
|
||||||
|
if payment_method:
|
||||||
|
qs = qs.filter(payment_method=payment_method)
|
||||||
|
|
||||||
|
payments = qs[:200]
|
||||||
|
return Response({
|
||||||
|
'results': [
|
||||||
|
{
|
||||||
|
'id': pay.id,
|
||||||
|
'account_name': pay.account.name if pay.account else None,
|
||||||
|
'amount': str(pay.amount),
|
||||||
|
'currency': pay.currency,
|
||||||
|
'status': pay.status,
|
||||||
|
'payment_method': pay.payment_method,
|
||||||
|
'created_at': pay.created_at.isoformat(),
|
||||||
|
'invoice_id': pay.invoice_id,
|
||||||
|
'invoice_number': pay.invoice.invoice_number if pay.invoice else None,
|
||||||
|
'transaction_reference': pay.transaction_reference,
|
||||||
|
}
|
||||||
|
for pay in payments
|
||||||
|
],
|
||||||
|
'count': qs.count(),
|
||||||
|
})
|
||||||
|
|
||||||
|
@action(detail=False, methods=['get'])
|
||||||
|
def pending_payments(self, request):
|
||||||
|
"""List payments pending approval"""
|
||||||
|
error = self._require_admin(request)
|
||||||
|
if error:
|
||||||
|
return error
|
||||||
|
|
||||||
payments = PaymentService.get_pending_approvals()
|
payments = PaymentService.get_pending_approvals()
|
||||||
|
|
||||||
@@ -317,11 +413,9 @@ class AdminBillingViewSet(viewsets.ViewSet):
|
|||||||
@action(detail=True, methods=['post'])
|
@action(detail=True, methods=['post'])
|
||||||
def approve_payment(self, request, pk=None):
|
def approve_payment(self, request, pk=None):
|
||||||
"""Approve a manual payment"""
|
"""Approve a manual payment"""
|
||||||
if not request.user.is_staff:
|
error = self._require_admin(request)
|
||||||
return Response(
|
if error:
|
||||||
{'error': 'Admin access required'},
|
return error
|
||||||
status=status.HTTP_403_FORBIDDEN
|
|
||||||
)
|
|
||||||
|
|
||||||
payment = get_object_or_404(Payment, id=pk)
|
payment = get_object_or_404(Payment, id=pk)
|
||||||
admin_notes = request.data.get('notes')
|
admin_notes = request.data.get('notes')
|
||||||
@@ -347,11 +441,9 @@ class AdminBillingViewSet(viewsets.ViewSet):
|
|||||||
@action(detail=True, methods=['post'])
|
@action(detail=True, methods=['post'])
|
||||||
def reject_payment(self, request, pk=None):
|
def reject_payment(self, request, pk=None):
|
||||||
"""Reject a manual payment"""
|
"""Reject a manual payment"""
|
||||||
if not request.user.is_staff:
|
error = self._require_admin(request)
|
||||||
return Response(
|
if error:
|
||||||
{'error': 'Admin access required'},
|
return error
|
||||||
status=status.HTTP_403_FORBIDDEN
|
|
||||||
)
|
|
||||||
|
|
||||||
payment = get_object_or_404(Payment, id=pk)
|
payment = get_object_or_404(Payment, id=pk)
|
||||||
rejection_reason = request.data.get('reason', 'No reason provided')
|
rejection_reason = request.data.get('reason', 'No reason provided')
|
||||||
@@ -377,11 +469,9 @@ class AdminBillingViewSet(viewsets.ViewSet):
|
|||||||
@action(detail=False, methods=['get'])
|
@action(detail=False, methods=['get'])
|
||||||
def stats(self, request):
|
def stats(self, request):
|
||||||
"""System billing stats"""
|
"""System billing stats"""
|
||||||
if not request.user.is_staff:
|
error = self._require_admin(request)
|
||||||
return Response(
|
if error:
|
||||||
{'error': 'Admin access required'},
|
return error
|
||||||
status=status.HTTP_403_FORBIDDEN
|
|
||||||
)
|
|
||||||
|
|
||||||
from django.db.models import Sum, Count
|
from django.db.models import Sum, Count
|
||||||
from ...auth.models import Account
|
from ...auth.models import Account
|
||||||
@@ -407,12 +497,12 @@ class AdminBillingViewSet(viewsets.ViewSet):
|
|||||||
|
|
||||||
# Revenue stats
|
# Revenue stats
|
||||||
total_revenue = Payment.objects.filter(
|
total_revenue = Payment.objects.filter(
|
||||||
status='completed',
|
status__in=['completed', 'succeeded'],
|
||||||
amount__gt=0
|
amount__gt=0
|
||||||
).aggregate(total=Sum('amount'))['total'] or 0
|
).aggregate(total=Sum('amount'))['total'] or 0
|
||||||
|
|
||||||
revenue_this_month = Payment.objects.filter(
|
revenue_this_month = Payment.objects.filter(
|
||||||
status='completed',
|
status__in=['completed', 'succeeded'],
|
||||||
processed_at__gte=this_month_start,
|
processed_at__gte=this_month_start,
|
||||||
amount__gt=0
|
amount__gt=0
|
||||||
).aggregate(total=Sum('amount'))['total'] or 0
|
).aggregate(total=Sum('amount'))['total'] or 0
|
||||||
@@ -430,9 +520,7 @@ class AdminBillingViewSet(viewsets.ViewSet):
|
|||||||
).aggregate(total=Sum('amount'))['total'] or 0)
|
).aggregate(total=Sum('amount'))['total'] or 0)
|
||||||
|
|
||||||
# Payment/Invoice stats
|
# Payment/Invoice stats
|
||||||
pending_approvals = Payment.objects.filter(
|
pending_approvals = Payment.objects.filter(status='pending_approval').count()
|
||||||
status='pending_approval'
|
|
||||||
).count()
|
|
||||||
|
|
||||||
invoices_pending = Invoice.objects.filter(status='pending').count()
|
invoices_pending = Invoice.objects.filter(status='pending').count()
|
||||||
invoices_overdue = Invoice.objects.filter(
|
invoices_overdue = Invoice.objects.filter(
|
||||||
@@ -442,7 +530,7 @@ class AdminBillingViewSet(viewsets.ViewSet):
|
|||||||
|
|
||||||
# Recent activity
|
# Recent activity
|
||||||
recent_payments = Payment.objects.filter(
|
recent_payments = Payment.objects.filter(
|
||||||
status='completed'
|
status__in=['completed', 'succeeded']
|
||||||
).order_by('-processed_at')[:5]
|
).order_by('-processed_at')[:5]
|
||||||
|
|
||||||
recent_activity = [
|
recent_activity = [
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# Generated by Django 5.2.8 on 2025-12-05 07:59
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('billing', '0004_add_invoice_payment_models'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='credittransaction',
|
||||||
|
name='reference_id',
|
||||||
|
field=models.CharField(blank=True, help_text='Optional reference (e.g., payment id, invoice id)', max_length=255),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='invoice',
|
||||||
|
name='billing_email',
|
||||||
|
field=models.EmailField(blank=True, max_length=254, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='invoice',
|
||||||
|
name='billing_period_end',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='invoice',
|
||||||
|
name='billing_period_start',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='invoice',
|
||||||
|
name='currency',
|
||||||
|
field=models.CharField(default='USD', max_length=3),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='payment',
|
||||||
|
name='admin_notes',
|
||||||
|
field=models.TextField(blank=True, help_text='Internal notes on approval/rejection'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='payment',
|
||||||
|
name='transaction_reference',
|
||||||
|
field=models.CharField(blank=True, max_length=255),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='invoice',
|
||||||
|
name='subtotal',
|
||||||
|
field=models.DecimalField(decimal_places=2, default=0, max_digits=10),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='invoice',
|
||||||
|
name='total',
|
||||||
|
field=models.DecimalField(decimal_places=2, default=0, max_digits=10),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='payment',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(choices=[('pending', 'Pending'), ('pending_approval', 'Pending Approval'), ('processing', 'Processing'), ('succeeded', 'Succeeded'), ('completed', 'Completed'), ('failed', 'Failed'), ('refunded', 'Refunded'), ('cancelled', 'Cancelled')], db_index=True, default='pending', max_length=20),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -65,7 +65,7 @@ const AdminBilling: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const [statsData, usersData, configsData] = await Promise.all([
|
const [statsData, usersData, configsData] = await Promise.all([
|
||||||
fetchAPI('/v1/admin/billing/stats/'),
|
fetchAPI('/v1/billing/admin/stats/'),
|
||||||
fetchAPI('/v1/admin/users/?limit=100'),
|
fetchAPI('/v1/admin/users/?limit=100'),
|
||||||
fetchAPI('/v1/admin/credit-costs/'),
|
fetchAPI('/v1/admin/credit-costs/'),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ export default function AccountBillingPage() {
|
|||||||
<h3 className="text-lg font-semibold mb-4">Quick Actions</h3>
|
<h3 className="text-lg font-semibold mb-4">Quick Actions</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Link
|
<Link
|
||||||
to="/account/purchase-credits"
|
to="/account/credits/purchase"
|
||||||
className="block w-full bg-blue-600 text-white text-center py-2 px-4 rounded hover:bg-blue-700 transition-colors"
|
className="block w-full bg-blue-600 text-white text-center py-2 px-4 rounded hover:bg-blue-700 transition-colors"
|
||||||
>
|
>
|
||||||
Purchase Credits
|
Purchase Credits
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ export default function PurchaseCreditsPage() {
|
|||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
await createManualPayment({
|
await createManualPayment({
|
||||||
|
invoice_id: invoiceData?.invoice_id || invoiceData?.id,
|
||||||
amount: String(selectedPackage?.price || 0),
|
amount: String(selectedPackage?.price || 0),
|
||||||
payment_method: selectedPaymentMethod as 'stripe' | 'paypal' | 'bank_transfer' | 'local_wallet',
|
payment_method: selectedPaymentMethod as 'stripe' | 'paypal' | 'bank_transfer' | 'local_wallet',
|
||||||
reference: manualPaymentData.transaction_reference,
|
reference: manualPaymentData.transaction_reference,
|
||||||
@@ -356,7 +357,7 @@ export default function PurchaseCreditsPage() {
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{paymentMethods.map((method) => (
|
{paymentMethods.map((method) => (
|
||||||
<div
|
<div
|
||||||
key={method.type}
|
key={method.id || method.type}
|
||||||
onClick={() => setSelectedPaymentMethod(method.type)}
|
onClick={() => setSelectedPaymentMethod(method.type)}
|
||||||
className={`cursor-pointer rounded-lg border-2 p-4 transition-all ${
|
className={`cursor-pointer rounded-lg border-2 p-4 transition-all ${
|
||||||
selectedPaymentMethod === method.type
|
selectedPaymentMethod === method.type
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { useState, useEffect } from 'react';
|
|||||||
import { Search, Filter, Loader2, AlertCircle, Download } from 'lucide-react';
|
import { Search, Filter, Loader2, AlertCircle, Download } from 'lucide-react';
|
||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
import Badge from '../../components/ui/badge/Badge';
|
import Badge from '../../components/ui/badge/Badge';
|
||||||
import { getInvoices, type Invoice } from '../../services/billing.api';
|
import { getAdminInvoices, type Invoice } from '../../services/billing.api';
|
||||||
|
|
||||||
export default function AdminAllInvoicesPage() {
|
export default function AdminAllInvoicesPage() {
|
||||||
const [invoices, setInvoices] = useState<Invoice[]>([]);
|
const [invoices, setInvoices] = useState<Invoice[]>([]);
|
||||||
@@ -23,7 +23,7 @@ export default function AdminAllInvoicesPage() {
|
|||||||
const loadInvoices = async () => {
|
const loadInvoices = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const data = await getInvoices({});
|
const data = await getAdminInvoices({});
|
||||||
setInvoices(data.results || []);
|
setInvoices(data.results || []);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.message || 'Failed to load invoices');
|
setError(err.message || 'Failed to load invoices');
|
||||||
@@ -99,6 +99,9 @@ export default function AdminAllInvoicesPage() {
|
|||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Invoice #
|
Invoice #
|
||||||
</th>
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Account
|
||||||
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Date
|
Date
|
||||||
</th>
|
</th>
|
||||||
@@ -126,6 +129,9 @@ export default function AdminAllInvoicesPage() {
|
|||||||
<td className="px-6 py-4 font-medium text-gray-900 dark:text-white">
|
<td className="px-6 py-4 font-medium text-gray-900 dark:text-white">
|
||||||
{invoice.invoice_number}
|
{invoice.invoice_number}
|
||||||
</td>
|
</td>
|
||||||
|
<td className="px-6 py-4 text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
{invoice.account_name || '—'}
|
||||||
|
</td>
|
||||||
<td className="px-6 py-4 text-sm text-gray-600 dark:text-gray-400">
|
<td className="px-6 py-4 text-sm text-gray-600 dark:text-gray-400">
|
||||||
{new Date(invoice.created_at).toLocaleDateString()}
|
{new Date(invoice.created_at).toLocaleDateString()}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -7,20 +7,12 @@ import { useState, useEffect } from 'react';
|
|||||||
import { Search, Filter, Loader2, AlertCircle } from 'lucide-react';
|
import { Search, Filter, Loader2, AlertCircle } from 'lucide-react';
|
||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
import Badge from '../../components/ui/badge/Badge';
|
import Badge from '../../components/ui/badge/Badge';
|
||||||
import { fetchAPI } from '../../services/api';
|
import { getAdminPayments, type Payment } from '../../services/billing.api';
|
||||||
|
|
||||||
interface Payment {
|
type AdminPayment = Payment & { account_name?: string };
|
||||||
id: number;
|
|
||||||
account_name: string;
|
|
||||||
amount: string;
|
|
||||||
currency: string;
|
|
||||||
status: string;
|
|
||||||
payment_method: string;
|
|
||||||
created_at: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AdminAllPaymentsPage() {
|
export default function AdminAllPaymentsPage() {
|
||||||
const [payments, setPayments] = useState<Payment[]>([]);
|
const [payments, setPayments] = useState<AdminPayment[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string>('');
|
const [error, setError] = useState<string>('');
|
||||||
const [statusFilter, setStatusFilter] = useState('all');
|
const [statusFilter, setStatusFilter] = useState('all');
|
||||||
@@ -32,7 +24,7 @@ export default function AdminAllPaymentsPage() {
|
|||||||
const loadPayments = async () => {
|
const loadPayments = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const data = await fetchAPI('/v1/admin/payments/');
|
const data = await getAdminPayments();
|
||||||
setPayments(data.results || []);
|
setPayments(data.results || []);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.message || 'Failed to load payments');
|
setError(err.message || 'Failed to load payments');
|
||||||
@@ -45,6 +37,22 @@ export default function AdminAllPaymentsPage() {
|
|||||||
return statusFilter === 'all' || payment.status === statusFilter;
|
return statusFilter === 'all' || payment.status === statusFilter;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'succeeded':
|
||||||
|
case 'completed':
|
||||||
|
return 'success';
|
||||||
|
case 'processing':
|
||||||
|
case 'pending':
|
||||||
|
case 'pending_approval':
|
||||||
|
return 'warning';
|
||||||
|
case 'refunded':
|
||||||
|
return 'info';
|
||||||
|
default:
|
||||||
|
return 'error';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
@@ -77,9 +85,13 @@ export default function AdminAllPaymentsPage() {
|
|||||||
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800"
|
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800"
|
||||||
>
|
>
|
||||||
<option value="all">All Status</option>
|
<option value="all">All Status</option>
|
||||||
|
<option value="pending_approval">Pending Approval</option>
|
||||||
|
<option value="processing">Processing</option>
|
||||||
<option value="succeeded">Succeeded</option>
|
<option value="succeeded">Succeeded</option>
|
||||||
|
<option value="completed">Completed</option>
|
||||||
<option value="pending">Pending</option>
|
<option value="pending">Pending</option>
|
||||||
<option value="failed">Failed</option>
|
<option value="failed">Failed</option>
|
||||||
|
<option value="cancelled">Cancelled</option>
|
||||||
<option value="refunded">Refunded</option>
|
<option value="refunded">Refunded</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,6 +102,7 @@ export default function AdminAllPaymentsPage() {
|
|||||||
<thead className="bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
|
<thead className="bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Account</th>
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Account</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Invoice</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Amount</th>
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Amount</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Method</th>
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Method</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
|
||||||
@@ -106,15 +119,15 @@ export default function AdminAllPaymentsPage() {
|
|||||||
filteredPayments.map((payment) => (
|
filteredPayments.map((payment) => (
|
||||||
<tr key={payment.id} className="hover:bg-gray-50 dark:hover:bg-gray-800">
|
<tr key={payment.id} className="hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||||
<td className="px-6 py-4 font-medium">{payment.account_name}</td>
|
<td className="px-6 py-4 font-medium">{payment.account_name}</td>
|
||||||
|
<td className="px-6 py-4 text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
{payment.invoice_number || payment.invoice_id || '—'}
|
||||||
|
</td>
|
||||||
<td className="px-6 py-4 font-semibold">{payment.currency} {payment.amount}</td>
|
<td className="px-6 py-4 font-semibold">{payment.currency} {payment.amount}</td>
|
||||||
<td className="px-6 py-4 text-sm">{payment.payment_method}</td>
|
<td className="px-6 py-4 text-sm capitalize">{payment.payment_method.replace('_', ' ')}</td>
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
<Badge
|
<Badge
|
||||||
variant="light"
|
variant="light"
|
||||||
color={
|
color={getStatusColor(payment.status)}
|
||||||
payment.status === 'succeeded' ? 'success' :
|
|
||||||
payment.status === 'pending' ? 'warning' : 'error'
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{payment.status}
|
{payment.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@@ -111,14 +111,16 @@ export interface Invoice {
|
|||||||
stripe_invoice_id?: string;
|
stripe_invoice_id?: string;
|
||||||
billing_period_start?: string;
|
billing_period_start?: string;
|
||||||
billing_period_end?: string;
|
billing_period_end?: string;
|
||||||
|
account_name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Payment {
|
export interface Payment {
|
||||||
id: number;
|
id: number;
|
||||||
invoice_id: number;
|
invoice_id: number;
|
||||||
|
invoice_number?: string;
|
||||||
amount: string;
|
amount: string;
|
||||||
currency: string;
|
currency: string;
|
||||||
status: 'pending' | 'processing' | 'succeeded' | 'failed' | 'refunded' | 'cancelled' | 'pending_approval';
|
status: 'pending' | 'processing' | 'succeeded' | 'failed' | 'refunded' | 'cancelled' | 'pending_approval' | 'completed';
|
||||||
payment_method: 'stripe' | 'paypal' | 'bank_transfer' | 'local_wallet' | 'manual';
|
payment_method: 'stripe' | 'paypal' | 'bank_transfer' | 'local_wallet' | 'manual';
|
||||||
created_at: string;
|
created_at: string;
|
||||||
processed_at?: string;
|
processed_at?: string;
|
||||||
@@ -186,7 +188,7 @@ export interface PendingPayment extends Payment {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export async function getCreditBalance(): Promise<CreditBalance> {
|
export async function getCreditBalance(): Promise<CreditBalance> {
|
||||||
return fetchAPI('/v1/billing/credits/balance/balance/');
|
return fetchAPI('/v1/billing/transactions/balance/');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCreditTransactions(): Promise<{
|
export async function getCreditTransactions(): Promise<{
|
||||||
@@ -259,7 +261,33 @@ export async function getCreditUsageLimits(): Promise<{
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export async function getAdminBillingStats(): Promise<AdminBillingStats> {
|
export async function getAdminBillingStats(): Promise<AdminBillingStats> {
|
||||||
return fetchAPI('/v1/admin/billing/stats/');
|
return fetchAPI('/v1/billing/admin/stats/');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAdminInvoices(params?: { status?: string; account_id?: number; search?: string }): Promise<{
|
||||||
|
results: Invoice[];
|
||||||
|
count: number;
|
||||||
|
}> {
|
||||||
|
const queryParams = new URLSearchParams();
|
||||||
|
if (params?.status) queryParams.append('status', params.status);
|
||||||
|
if (params?.account_id) queryParams.append('account_id', String(params.account_id));
|
||||||
|
if (params?.search) queryParams.append('search', params.search);
|
||||||
|
|
||||||
|
const url = `/v1/billing/admin/invoices/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
||||||
|
return fetchAPI(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAdminPayments(params?: { status?: string; account_id?: number; payment_method?: string }): Promise<{
|
||||||
|
results: Payment[];
|
||||||
|
count: number;
|
||||||
|
}> {
|
||||||
|
const queryParams = new URLSearchParams();
|
||||||
|
if (params?.status) queryParams.append('status', params.status);
|
||||||
|
if (params?.account_id) queryParams.append('account_id', String(params.account_id));
|
||||||
|
if (params?.payment_method) queryParams.append('payment_method', params.payment_method);
|
||||||
|
|
||||||
|
const url = `/v1/billing/admin/payments/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
||||||
|
return fetchAPI(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAdminUsers(params?: {
|
export async function getAdminUsers(params?: {
|
||||||
@@ -376,7 +404,7 @@ export async function getPayments(params?: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function submitManualPayment(data: {
|
export async function submitManualPayment(data: {
|
||||||
invoice_id: number;
|
invoice_id?: number;
|
||||||
payment_method: 'bank_transfer' | 'local_wallet' | 'manual';
|
payment_method: 'bank_transfer' | 'local_wallet' | 'manual';
|
||||||
amount: string;
|
amount: string;
|
||||||
currency?: string;
|
currency?: string;
|
||||||
@@ -409,9 +437,12 @@ export async function purchaseCreditPackage(data: {
|
|||||||
package_id: number;
|
package_id: number;
|
||||||
payment_method: 'stripe' | 'paypal' | 'bank_transfer' | 'local_wallet';
|
payment_method: 'stripe' | 'paypal' | 'bank_transfer' | 'local_wallet';
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
id: number;
|
invoice_id?: number;
|
||||||
status: string;
|
invoice_number?: string;
|
||||||
|
total_amount?: string;
|
||||||
|
status?: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
|
next_action?: string;
|
||||||
stripe_client_secret?: string;
|
stripe_client_secret?: string;
|
||||||
paypal_order_id?: string;
|
paypal_order_id?: string;
|
||||||
}> {
|
}> {
|
||||||
@@ -467,6 +498,7 @@ export async function getAvailablePaymentMethods(): Promise<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createManualPayment(data: {
|
export async function createManualPayment(data: {
|
||||||
|
invoice_id?: number;
|
||||||
amount: string;
|
amount: string;
|
||||||
payment_method: string;
|
payment_method: string;
|
||||||
reference: string;
|
reference: string;
|
||||||
@@ -490,7 +522,7 @@ export async function getPendingPayments(): Promise<{
|
|||||||
results: PendingPayment[];
|
results: PendingPayment[];
|
||||||
count: number;
|
count: number;
|
||||||
}> {
|
}> {
|
||||||
return fetchAPI('/v1/admin/payments/pending/');
|
return fetchAPI('/v1/billing/admin/pending_payments/');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function approvePayment(paymentId: number, data?: {
|
export async function approvePayment(paymentId: number, data?: {
|
||||||
@@ -499,7 +531,7 @@ export async function approvePayment(paymentId: number, data?: {
|
|||||||
message: string;
|
message: string;
|
||||||
payment: Payment;
|
payment: Payment;
|
||||||
}> {
|
}> {
|
||||||
return fetchAPI(`/v1/admin/payments/${paymentId}/approve/`, {
|
return fetchAPI(`/v1/billing/admin/${paymentId}/approve_payment/`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(data || {}),
|
body: JSON.stringify(data || {}),
|
||||||
});
|
});
|
||||||
@@ -512,7 +544,7 @@ export async function rejectPayment(paymentId: number, data: {
|
|||||||
message: string;
|
message: string;
|
||||||
payment: Payment;
|
payment: Payment;
|
||||||
}> {
|
}> {
|
||||||
return fetchAPI(`/v1/admin/payments/${paymentId}/reject/`, {
|
return fetchAPI(`/v1/billing/admin/${paymentId}/reject_payment/`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user