billing admin account 1

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-05 08:01:55 +00:00
parent f91037b729
commit 1e718105f2
12 changed files with 378 additions and 85 deletions

View File

@@ -222,7 +222,7 @@ class UsageAnalyticsViewSet(viewsets.ViewSet):
'period_days': days,
'start_date': start_date.isoformat(),
'end_date': timezone.now().isoformat(),
'current_balance': account.credit_balance,
'current_balance': account.credits,
'usage_by_type': list(usage_by_type),
'purchases_by_type': list(purchases_by_type),
'daily_usage': daily_usage,

View File

@@ -1,6 +1,7 @@
"""
Billing Models for Credit System
"""
from decimal import Decimal
from django.db import models
from django.core.validators import MinValueValidator
from django.conf import settings
@@ -22,6 +23,11 @@ class CreditTransaction(AccountBaseModel):
balance_after = models.IntegerField(help_text="Credit balance after this transaction")
description = models.CharField(max_length=255)
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)
class Meta:
@@ -183,9 +189,10 @@ class Invoice(AccountBaseModel):
)
# 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)
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 = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', db_index=True)
@@ -201,6 +208,9 @@ class Invoice(AccountBaseModel):
# 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)
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
notes = models.TextField(blank=True)
@@ -222,6 +232,45 @@ class Invoice(AccountBaseModel):
def __str__(self):
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):
"""
@@ -230,8 +279,10 @@ class Payment(AccountBaseModel):
"""
STATUS_CHOICES = [
('pending', 'Pending'),
('pending_approval', 'Pending Approval'),
('processing', 'Processing'),
('succeeded', 'Succeeded'),
('completed', 'Completed'), # Legacy alias for succeeded
('failed', 'Failed'),
('refunded', 'Refunded'),
('cancelled', 'Cancelled'),
@@ -272,6 +323,8 @@ class Payment(AccountBaseModel):
help_text="Bank transfer reference, wallet transaction ID, etc."
)
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(
settings.AUTH_USER_MODEL,
null=True,

View File

@@ -52,6 +52,8 @@ class InvoiceService:
billing_email=account.billing_email or account.users.filter(role='owner').first().email,
status='pending',
currency='USD',
invoice_date=timezone.now().date(),
due_date=billing_period_end.date(),
billing_period_start=billing_period_start,
billing_period_end=billing_period_end
)
@@ -83,7 +85,13 @@ class InvoiceService:
invoice_number=InvoiceService.generate_invoice_number(account),
billing_email=account.billing_email or account.users.filter(role='owner').first().email,
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
@@ -125,6 +133,7 @@ class InvoiceService:
status='draft',
currency='USD',
notes=notes,
invoice_date=timezone.now().date(),
due_date=due_date or (timezone.now() + timedelta(days=30))
)

View File

@@ -77,6 +77,12 @@ class PaymentService:
if payment_method not in ['bank_transfer', 'local_wallet', 'manual']:
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(
account=invoice.account,
invoice=invoice,
@@ -86,7 +92,7 @@ class PaymentService:
status='pending_approval',
transaction_reference=transaction_reference,
admin_notes=admin_notes,
metadata=metadata or {}
metadata=meta
)
return payment
@@ -102,7 +108,7 @@ class PaymentService:
"""
from .invoice_service import InvoiceService
payment.status = 'completed'
payment.status = 'succeeded'
payment.processed_at = timezone.now()
if transaction_id:
@@ -153,9 +159,10 @@ class PaymentService:
if payment.status != 'pending_approval':
raise ValueError("Payment is not pending approval")
payment.status = 'completed'
payment.status = 'succeeded'
payment.processed_at = timezone.now()
payment.approved_by_id = approved_by_user_id
payment.approved_at = timezone.now()
if 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:
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
CreditTransaction.objects.create(
account=payment.account,
@@ -244,14 +256,20 @@ class PaymentService:
return {
'methods': [
{
'id': 'stripe-default',
'type': 'stripe',
'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',
'name': 'PayPal',
'instructions': 'Pay with your PayPal account'
'display_name': 'PayPal',
'instructions': 'Pay with your PayPal account',
'is_enabled': True,
}
],
'stripe': True,
@@ -272,9 +290,12 @@ class PaymentService:
for config in configs:
method_flags[config.payment_method] = True
method_data = {
'id': f"{config.country_code}-{config.payment_method}-{config.id}",
'type': config.payment_method,
'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
@@ -323,8 +344,8 @@ class PaymentService:
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")
if payment.status not in ['completed', 'succeeded']:
raise ValueError("Can only refund succeeded/complete payments")
refund_amount = amount or payment.amount
@@ -338,8 +359,9 @@ class PaymentService:
amount=-refund_amount, # Negative amount for refund
currency=payment.currency,
payment_method=payment.payment_method,
status='completed',
status='refunded',
processed_at=timezone.now(),
refunded_at=timezone.now(),
metadata={
'refund_for_payment_id': payment.id,
'refund_reason': reason,
@@ -348,10 +370,16 @@ class PaymentService:
)
# Update original payment metadata
payment.metadata['refunded'] = True
payment.metadata['refund_payment_id'] = refund.id
payment.metadata['refund_amount'] = str(refund_amount)
payment.save()
meta = payment.metadata or {}
meta['refunded'] = True
meta['refund_payment_id'] = refund.id
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

View File

@@ -8,6 +8,7 @@ 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 django.db import models
from .models import Invoice, Payment, CreditPackage, PaymentMethodConfig, CreditTransaction
from .services.invoice_service import InvoiceService
@@ -126,16 +127,21 @@ class PaymentViewSet(viewsets.ViewSet):
"""Get available payment methods for current account"""
account = request.user.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):
"""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')
transaction_reference = request.data.get('transaction_reference') or request.data.get('reference')
notes = request.data.get('notes')
if not all([invoice_id, payment_method, transaction_reference]):
@@ -261,22 +267,32 @@ class CreditTransactionViewSet(viewsets.ViewSet):
for txn in transactions
],
'count': transactions.count(),
'current_balance': account.credit_balance
'current_balance': account.credits
})
@action(detail=False, methods=['get'])
def balance(self, request):
"""Get current credit balance"""
account = request.user.account
# Get subscription details
active_subscription = account.subscriptions.filter(status='active').first()
from django.utils import timezone
from datetime import timedelta
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({
'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
'credits': account.credits,
'plan_credits_per_month': included,
'credits_used_this_month': used_this_month,
'credits_remaining': max(account.credits, 0),
})
@@ -284,15 +300,95 @@ class AdminBillingViewSet(viewsets.ViewSet):
"""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:
def _require_admin(self, request):
if not request.user.is_staff and not getattr(request.user, 'is_superuser', False):
return Response(
{'error': 'Admin access required'},
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()
@@ -317,11 +413,9 @@ class AdminBillingViewSet(viewsets.ViewSet):
@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
)
error = self._require_admin(request)
if error:
return error
payment = get_object_or_404(Payment, id=pk)
admin_notes = request.data.get('notes')
@@ -347,11 +441,9 @@ class AdminBillingViewSet(viewsets.ViewSet):
@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
)
error = self._require_admin(request)
if error:
return error
payment = get_object_or_404(Payment, id=pk)
rejection_reason = request.data.get('reason', 'No reason provided')
@@ -377,11 +469,9 @@ class AdminBillingViewSet(viewsets.ViewSet):
@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
)
error = self._require_admin(request)
if error:
return error
from django.db.models import Sum, Count
from ...auth.models import Account
@@ -407,12 +497,12 @@ class AdminBillingViewSet(viewsets.ViewSet):
# Revenue stats
total_revenue = Payment.objects.filter(
status='completed',
status__in=['completed', 'succeeded'],
amount__gt=0
).aggregate(total=Sum('amount'))['total'] or 0
revenue_this_month = Payment.objects.filter(
status='completed',
status__in=['completed', 'succeeded'],
processed_at__gte=this_month_start,
amount__gt=0
).aggregate(total=Sum('amount'))['total'] or 0
@@ -430,9 +520,7 @@ class AdminBillingViewSet(viewsets.ViewSet):
).aggregate(total=Sum('amount'))['total'] or 0)
# Payment/Invoice stats
pending_approvals = Payment.objects.filter(
status='pending_approval'
).count()
pending_approvals = Payment.objects.filter(status='pending_approval').count()
invoices_pending = Invoice.objects.filter(status='pending').count()
invoices_overdue = Invoice.objects.filter(
@@ -442,7 +530,7 @@ class AdminBillingViewSet(viewsets.ViewSet):
# Recent activity
recent_payments = Payment.objects.filter(
status='completed'
status__in=['completed', 'succeeded']
).order_by('-processed_at')[:5]
recent_activity = [

View File

@@ -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),
),
]

View File

@@ -65,7 +65,7 @@ const AdminBilling: React.FC = () => {
try {
setLoading(true);
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/credit-costs/'),
]);

View File

@@ -205,7 +205,7 @@ export default function AccountBillingPage() {
<h3 className="text-lg font-semibold mb-4">Quick Actions</h3>
<div className="space-y-2">
<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"
>
Purchase Credits

View File

@@ -104,6 +104,7 @@ export default function PurchaseCreditsPage() {
setError('');
await createManualPayment({
invoice_id: invoiceData?.invoice_id || invoiceData?.id,
amount: String(selectedPackage?.price || 0),
payment_method: selectedPaymentMethod as 'stripe' | 'paypal' | 'bank_transfer' | 'local_wallet',
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">
{paymentMethods.map((method) => (
<div
key={method.type}
key={method.id || method.type}
onClick={() => setSelectedPaymentMethod(method.type)}
className={`cursor-pointer rounded-lg border-2 p-4 transition-all ${
selectedPaymentMethod === method.type

View File

@@ -7,7 +7,7 @@ import { useState, useEffect } from 'react';
import { Search, Filter, Loader2, AlertCircle, Download } from 'lucide-react';
import { Card } from '../../components/ui/card';
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() {
const [invoices, setInvoices] = useState<Invoice[]>([]);
@@ -23,7 +23,7 @@ export default function AdminAllInvoicesPage() {
const loadInvoices = async () => {
try {
setLoading(true);
const data = await getInvoices({});
const data = await getAdminInvoices({});
setInvoices(data.results || []);
} catch (err: any) {
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">
Invoice #
</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">
Date
</th>
@@ -126,6 +129,9 @@ export default function AdminAllInvoicesPage() {
<td className="px-6 py-4 font-medium text-gray-900 dark:text-white">
{invoice.invoice_number}
</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">
{new Date(invoice.created_at).toLocaleDateString()}
</td>

View File

@@ -7,20 +7,12 @@ import { useState, useEffect } from 'react';
import { Search, Filter, Loader2, AlertCircle } from 'lucide-react';
import { Card } from '../../components/ui/card';
import Badge from '../../components/ui/badge/Badge';
import { fetchAPI } from '../../services/api';
import { getAdminPayments, type Payment } from '../../services/billing.api';
interface Payment {
id: number;
account_name: string;
amount: string;
currency: string;
status: string;
payment_method: string;
created_at: string;
}
type AdminPayment = Payment & { account_name?: string };
export default function AdminAllPaymentsPage() {
const [payments, setPayments] = useState<Payment[]>([]);
const [payments, setPayments] = useState<AdminPayment[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string>('');
const [statusFilter, setStatusFilter] = useState('all');
@@ -32,7 +24,7 @@ export default function AdminAllPaymentsPage() {
const loadPayments = async () => {
try {
setLoading(true);
const data = await fetchAPI('/v1/admin/payments/');
const data = await getAdminPayments();
setPayments(data.results || []);
} catch (err: any) {
setError(err.message || 'Failed to load payments');
@@ -45,6 +37,22 @@ export default function AdminAllPaymentsPage() {
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) {
return (
<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"
>
<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="completed">Completed</option>
<option value="pending">Pending</option>
<option value="failed">Failed</option>
<option value="cancelled">Cancelled</option>
<option value="refunded">Refunded</option>
</select>
</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">
<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">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">Method</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) => (
<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 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 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">
<Badge
variant="light"
color={
payment.status === 'succeeded' ? 'success' :
payment.status === 'pending' ? 'warning' : 'error'
}
color={getStatusColor(payment.status)}
>
{payment.status}
</Badge>

View File

@@ -111,14 +111,16 @@ export interface Invoice {
stripe_invoice_id?: string;
billing_period_start?: string;
billing_period_end?: string;
account_name?: string;
}
export interface Payment {
id: number;
invoice_id: number;
invoice_number?: string;
amount: 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';
created_at: string;
processed_at?: string;
@@ -186,7 +188,7 @@ export interface PendingPayment extends Payment {
// ============================================================================
export async function getCreditBalance(): Promise<CreditBalance> {
return fetchAPI('/v1/billing/credits/balance/balance/');
return fetchAPI('/v1/billing/transactions/balance/');
}
export async function getCreditTransactions(): Promise<{
@@ -259,7 +261,33 @@ export async function getCreditUsageLimits(): Promise<{
// ============================================================================
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?: {
@@ -376,7 +404,7 @@ export async function getPayments(params?: {
}
export async function submitManualPayment(data: {
invoice_id: number;
invoice_id?: number;
payment_method: 'bank_transfer' | 'local_wallet' | 'manual';
amount: string;
currency?: string;
@@ -409,9 +437,12 @@ export async function purchaseCreditPackage(data: {
package_id: number;
payment_method: 'stripe' | 'paypal' | 'bank_transfer' | 'local_wallet';
}): Promise<{
id: number;
status: string;
invoice_id?: number;
invoice_number?: string;
total_amount?: string;
status?: string;
message?: string;
next_action?: string;
stripe_client_secret?: string;
paypal_order_id?: string;
}> {
@@ -467,6 +498,7 @@ export async function getAvailablePaymentMethods(): Promise<{
}
export async function createManualPayment(data: {
invoice_id?: number;
amount: string;
payment_method: string;
reference: string;
@@ -490,7 +522,7 @@ export async function getPendingPayments(): Promise<{
results: PendingPayment[];
count: number;
}> {
return fetchAPI('/v1/admin/payments/pending/');
return fetchAPI('/v1/billing/admin/pending_payments/');
}
export async function approvePayment(paymentId: number, data?: {
@@ -499,7 +531,7 @@ export async function approvePayment(paymentId: number, data?: {
message: string;
payment: Payment;
}> {
return fetchAPI(`/v1/admin/payments/${paymentId}/approve/`, {
return fetchAPI(`/v1/billing/admin/${paymentId}/approve_payment/`, {
method: 'POST',
body: JSON.stringify(data || {}),
});
@@ -512,7 +544,7 @@ export async function rejectPayment(paymentId: number, data: {
message: string;
payment: Payment;
}> {
return fetchAPI(`/v1/admin/payments/${paymentId}/reject/`, {
return fetchAPI(`/v1/billing/admin/${paymentId}/reject_payment/`, {
method: 'POST',
body: JSON.stringify(data),
});