From 1e718105f2af1b3021811d4970c67feb49619a67 Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Fri, 5 Dec 2025 08:01:55 +0000 Subject: [PATCH] billing admin account 1 --- backend/igny8_core/api/account_views.py | 2 +- backend/igny8_core/business/billing/models.py | 57 +++++- .../billing/services/invoice_service.py | 11 +- .../billing/services/payment_service.py | 54 ++++-- backend/igny8_core/business/billing/views.py | 162 ++++++++++++++---- ...rence_id_invoice_billing_email_and_more.py | 63 +++++++ frontend/src/pages/Admin/AdminBilling.tsx | 2 +- .../src/pages/account/AccountBillingPage.tsx | 2 +- .../src/pages/account/PurchaseCreditsPage.tsx | 3 +- .../src/pages/admin/AdminAllInvoicesPage.tsx | 10 +- .../src/pages/admin/AdminAllPaymentsPage.tsx | 47 +++-- frontend/src/services/billing.api.ts | 50 +++++- 12 files changed, 378 insertions(+), 85 deletions(-) create mode 100644 backend/igny8_core/modules/billing/migrations/0005_credittransaction_reference_id_invoice_billing_email_and_more.py diff --git a/backend/igny8_core/api/account_views.py b/backend/igny8_core/api/account_views.py index ef0d67fc..512d0943 100644 --- a/backend/igny8_core/api/account_views.py +++ b/backend/igny8_core/api/account_views.py @@ -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, diff --git a/backend/igny8_core/business/billing/models.py b/backend/igny8_core/business/billing/models.py index 057ed4ae..a5a09ab5 100644 --- a/backend/igny8_core/business/billing/models.py +++ b/backend/igny8_core/business/billing/models.py @@ -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, diff --git a/backend/igny8_core/business/billing/services/invoice_service.py b/backend/igny8_core/business/billing/services/invoice_service.py index 6cfb2906..3b463411 100644 --- a/backend/igny8_core/business/billing/services/invoice_service.py +++ b/backend/igny8_core/business/billing/services/invoice_service.py @@ -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)) ) diff --git a/backend/igny8_core/business/billing/services/payment_service.py b/backend/igny8_core/business/billing/services/payment_service.py index 98e6b811..816aa3f9 100644 --- a/backend/igny8_core/business/billing/services/payment_service.py +++ b/backend/igny8_core/business/billing/services/payment_service.py @@ -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 diff --git a/backend/igny8_core/business/billing/views.py b/backend/igny8_core/business/billing/views.py index 7485d8fc..03720ea4 100644 --- a/backend/igny8_core/business/billing/views.py +++ b/backend/igny8_core/business/billing/views.py @@ -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 = [ diff --git a/backend/igny8_core/modules/billing/migrations/0005_credittransaction_reference_id_invoice_billing_email_and_more.py b/backend/igny8_core/modules/billing/migrations/0005_credittransaction_reference_id_invoice_billing_email_and_more.py new file mode 100644 index 00000000..73093ea0 --- /dev/null +++ b/backend/igny8_core/modules/billing/migrations/0005_credittransaction_reference_id_invoice_billing_email_and_more.py @@ -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), + ), + ] diff --git a/frontend/src/pages/Admin/AdminBilling.tsx b/frontend/src/pages/Admin/AdminBilling.tsx index 92945101..53dc9a9e 100644 --- a/frontend/src/pages/Admin/AdminBilling.tsx +++ b/frontend/src/pages/Admin/AdminBilling.tsx @@ -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/'), ]); diff --git a/frontend/src/pages/account/AccountBillingPage.tsx b/frontend/src/pages/account/AccountBillingPage.tsx index af3db386..89605644 100644 --- a/frontend/src/pages/account/AccountBillingPage.tsx +++ b/frontend/src/pages/account/AccountBillingPage.tsx @@ -205,7 +205,7 @@ export default function AccountBillingPage() {

Quick Actions

Purchase Credits diff --git a/frontend/src/pages/account/PurchaseCreditsPage.tsx b/frontend/src/pages/account/PurchaseCreditsPage.tsx index d931199e..78f42511 100644 --- a/frontend/src/pages/account/PurchaseCreditsPage.tsx +++ b/frontend/src/pages/account/PurchaseCreditsPage.tsx @@ -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() {
{paymentMethods.map((method) => (
setSelectedPaymentMethod(method.type)} className={`cursor-pointer rounded-lg border-2 p-4 transition-all ${ selectedPaymentMethod === method.type diff --git a/frontend/src/pages/admin/AdminAllInvoicesPage.tsx b/frontend/src/pages/admin/AdminAllInvoicesPage.tsx index 82d2571d..23d5076f 100644 --- a/frontend/src/pages/admin/AdminAllInvoicesPage.tsx +++ b/frontend/src/pages/admin/AdminAllInvoicesPage.tsx @@ -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([]); @@ -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() { Invoice # + + Account + Date @@ -126,6 +129,9 @@ export default function AdminAllInvoicesPage() { {invoice.invoice_number} + + {invoice.account_name || '—'} + {new Date(invoice.created_at).toLocaleDateString()} diff --git a/frontend/src/pages/admin/AdminAllPaymentsPage.tsx b/frontend/src/pages/admin/AdminAllPaymentsPage.tsx index 416b70e9..7a0845a1 100644 --- a/frontend/src/pages/admin/AdminAllPaymentsPage.tsx +++ b/frontend/src/pages/admin/AdminAllPaymentsPage.tsx @@ -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([]); + const [payments, setPayments] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); 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 (
@@ -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" > + + + +
@@ -90,6 +102,7 @@ export default function AdminAllPaymentsPage() { Account + Invoice Amount Method Status @@ -106,15 +119,15 @@ export default function AdminAllPaymentsPage() { filteredPayments.map((payment) => ( {payment.account_name} + + {payment.invoice_number || payment.invoice_id || '—'} + {payment.currency} {payment.amount} - {payment.payment_method} + {payment.payment_method.replace('_', ' ')} {payment.status} diff --git a/frontend/src/services/billing.api.ts b/frontend/src/services/billing.api.ts index 58046654..76b7354f 100644 --- a/frontend/src/services/billing.api.ts +++ b/frontend/src/services/billing.api.ts @@ -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 { - 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 { - 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), });