From 4e764e208dba1083813e98c91797e5e2860a2504 Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Sun, 7 Dec 2025 04:28:46 +0000 Subject: [PATCH] billing and paymetn methods --- backend/igny8_core/admin/site.py | 9 +- backend/igny8_core/business/billing/views.py | 173 ++++ backend/igny8_core/modules/billing/admin.py | 3 +- .../igny8_core/modules/billing/admin_urls.py | 15 + backend/seed_credit_packages.py | 75 -- backend/seed_payment_configs.py | 125 --- docs/API/API-COMPLETE-REFERENCE.md | 11 + .../src/pages/account/PlansAndBillingPage.tsx | 4 +- .../src/pages/admin/AdminAllPaymentsPage.tsx | 763 ++++++++++++++++-- .../src/pages/admin/AdminSystemDashboard.tsx | 94 +-- frontend/src/services/billing.api.ts | 87 +- 11 files changed, 1010 insertions(+), 349 deletions(-) delete mode 100644 backend/seed_credit_packages.py delete mode 100644 backend/seed_payment_configs.py diff --git a/backend/igny8_core/admin/site.py b/backend/igny8_core/admin/site.py index 63db99d2..b046d37d 100644 --- a/backend/igny8_core/admin/site.py +++ b/backend/igny8_core/admin/site.py @@ -40,8 +40,6 @@ class Igny8AdminSite(admin.AdminSite): ('billing', 'Invoice'), ('billing', 'Payment'), ('billing', 'CreditPackage'), - ('billing', 'PaymentMethodConfig'), - ('billing', 'AccountPaymentMethod'), ('billing', 'CreditCostConfig'), ], }, @@ -107,6 +105,12 @@ class Igny8AdminSite(admin.AdminSite): ('automation', 'AutomationRun'), ], }, + 'Payments': { + 'models': [ + ('billing', 'PaymentMethodConfig'), + ('billing', 'AccountPaymentMethod'), + ], + }, 'Integrations & Sync': { 'models': [ ('integration', 'SiteIntegration'), @@ -170,6 +174,7 @@ class Igny8AdminSite(admin.AdminSite): 'Writer Module', 'Thinker Module', 'System Configuration', + 'Payments', 'Integrations & Sync', 'Publishing', 'Optimization', diff --git a/backend/igny8_core/business/billing/views.py b/backend/igny8_core/business/billing/views.py index 73dcb4de..11efee4d 100644 --- a/backend/igny8_core/business/billing/views.py +++ b/backend/igny8_core/business/billing/views.py @@ -545,6 +545,179 @@ class AdminBillingViewSet(viewsets.ViewSet): status=status.HTTP_400_BAD_REQUEST ) + @action(detail=False, methods=['get', 'post']) + def payment_method_configs(self, request): + """List/create payment method configs (country-level)""" + error = self._require_admin(request) + if error: + return error + + class PMConfigSerializer(serializers.ModelSerializer): + class Meta: + model = PaymentMethodConfig + fields = [ + 'id', + 'country_code', + 'payment_method', + 'display_name', + 'is_enabled', + 'instructions', + 'sort_order', + 'created_at', + 'updated_at', + ] + read_only_fields = ['id', 'created_at', 'updated_at'] + + if request.method.lower() == 'post': + serializer = PMConfigSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + obj = serializer.save() + return Response(PMConfigSerializer(obj).data, status=status.HTTP_201_CREATED) + + qs = PaymentMethodConfig.objects.all().order_by('country_code', 'sort_order', 'payment_method') + country = request.query_params.get('country_code') + method = request.query_params.get('payment_method') + if country: + qs = qs.filter(country_code=country) + if method: + qs = qs.filter(payment_method=method) + data = PMConfigSerializer(qs, many=True).data + return Response({'results': data, 'count': len(data)}) + + @action(detail=True, methods=['get', 'patch', 'put', 'delete'], url_path='payment_method_config') + @extend_schema(tags=['Admin Billing']) + def payment_method_config(self, request, pk=None): + """Retrieve/update/delete a payment method config""" + error = self._require_admin(request) + if error: + return error + + obj = get_object_or_404(PaymentMethodConfig, id=pk) + + class PMConfigSerializer(serializers.ModelSerializer): + class Meta: + model = PaymentMethodConfig + fields = [ + 'id', + 'country_code', + 'payment_method', + 'display_name', + 'is_enabled', + 'instructions', + 'sort_order', + 'created_at', + 'updated_at', + ] + read_only_fields = ['id', 'created_at', 'updated_at'] + + if request.method.lower() == 'get': + return Response(PMConfigSerializer(obj).data) + if request.method.lower() in ['patch', 'put']: + partial = request.method.lower() == 'patch' + serializer = PMConfigSerializer(obj, data=request.data, partial=partial) + serializer.is_valid(raise_exception=True) + obj = serializer.save() + return Response(PMConfigSerializer(obj).data) + # delete + obj.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + @action(detail=False, methods=['get', 'post']) + @extend_schema(tags=['Admin Billing']) + def account_payment_methods(self, request): + """List/create account payment methods (admin)""" + error = self._require_admin(request) + if error: + return error + + class AccountPMSerializer(serializers.ModelSerializer): + class Meta: + model = AccountPaymentMethod + fields = [ + 'id', + 'account', + 'type', + 'display_name', + 'is_default', + 'is_enabled', + 'is_verified', + 'country_code', + 'instructions', + 'metadata', + 'created_at', + 'updated_at', + ] + read_only_fields = ['id', 'is_verified', 'created_at', 'updated_at'] + + if request.method.lower() == 'post': + serializer = AccountPMSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + obj = serializer.save() + return Response(AccountPMSerializer(obj).data, status=status.HTTP_201_CREATED) + + qs = AccountPaymentMethod.objects.select_related('account').order_by('account_id', '-is_default', 'display_name') + account_id = request.query_params.get('account_id') + if account_id: + qs = qs.filter(account_id=account_id) + data = AccountPMSerializer(qs, many=True).data + return Response({'results': data, 'count': len(data)}) + + @action(detail=True, methods=['get', 'patch', 'put', 'delete'], url_path='account_payment_method') + @extend_schema(tags=['Admin Billing']) + def account_payment_method(self, request, pk=None): + """Retrieve/update/delete an account payment method (admin)""" + error = self._require_admin(request) + if error: + return error + + obj = get_object_or_404(AccountPaymentMethod, id=pk) + + class AccountPMSerializer(serializers.ModelSerializer): + class Meta: + model = AccountPaymentMethod + fields = [ + 'id', + 'account', + 'type', + 'display_name', + 'is_default', + 'is_enabled', + 'is_verified', + 'country_code', + 'instructions', + 'metadata', + 'created_at', + 'updated_at', + ] + read_only_fields = ['id', 'is_verified', 'created_at', 'updated_at'] + + if request.method.lower() == 'get': + return Response(AccountPMSerializer(obj).data) + if request.method.lower() in ['patch', 'put']: + partial = request.method.lower() == 'patch' + serializer = AccountPMSerializer(obj, data=request.data, partial=partial) + serializer.is_valid(raise_exception=True) + obj = serializer.save() + if serializer.validated_data.get('is_default'): + AccountPaymentMethod.objects.filter(account=obj.account).exclude(id=obj.id).update(is_default=False) + return Response(AccountPMSerializer(obj).data) + obj.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + @action(detail=True, methods=['post'], url_path='account_payment_method/set_default') + @extend_schema(tags=['Admin Billing']) + def set_default_account_payment_method(self, request, pk=None): + """Set default account payment method (admin)""" + error = self._require_admin(request) + if error: + return error + + obj = get_object_or_404(AccountPaymentMethod, id=pk) + AccountPaymentMethod.objects.filter(account=obj.account).update(is_default=False) + obj.is_default = True + obj.save(update_fields=['is_default']) + return Response({'message': 'Default payment method updated', 'id': obj.id}) + @action(detail=False, methods=['get']) def stats(self, request): """System billing stats""" diff --git a/backend/igny8_core/modules/billing/admin.py b/backend/igny8_core/modules/billing/admin.py index 08dd41f7..d78d0a86 100644 --- a/backend/igny8_core/modules/billing/admin.py +++ b/backend/igny8_core/modules/billing/admin.py @@ -94,9 +94,10 @@ class CreditPackageAdmin(admin.ModelAdmin): @admin.register(PaymentMethodConfig) class PaymentMethodConfigAdmin(admin.ModelAdmin): - list_display = ['country_code', 'payment_method', 'is_enabled', 'display_name', 'sort_order'] + list_display = ['country_code', 'payment_method', 'display_name', 'is_enabled', 'sort_order', 'updated_at'] list_filter = ['payment_method', 'is_enabled', 'country_code'] search_fields = ['country_code', 'display_name', 'payment_method'] + list_editable = ['is_enabled', 'sort_order'] readonly_fields = ['created_at', 'updated_at'] diff --git a/backend/igny8_core/modules/billing/admin_urls.py b/backend/igny8_core/modules/billing/admin_urls.py index 6ecf129f..22463663 100644 --- a/backend/igny8_core/modules/billing/admin_urls.py +++ b/backend/igny8_core/modules/billing/admin_urls.py @@ -19,6 +19,21 @@ urlpatterns = [ path('billing/pending_payments/', BillingAdminViewSet.as_view({'get': 'pending_payments'}), name='admin-billing-pending-payments'), path('billing//approve_payment/', BillingAdminViewSet.as_view({'post': 'approve_payment'}), name='admin-billing-approve-payment'), path('billing//reject_payment/', BillingAdminViewSet.as_view({'post': 'reject_payment'}), name='admin-billing-reject-payment'), + path('billing/payment-method-configs/', BillingAdminViewSet.as_view({'get': 'payment_method_configs', 'post': 'payment_method_configs'}), name='admin-billing-payment-method-configs'), + path('billing/payment-method-configs//', BillingAdminViewSet.as_view({ + 'get': 'payment_method_config', + 'patch': 'payment_method_config', + 'put': 'payment_method_config', + 'delete': 'payment_method_config', + }), name='admin-billing-payment-method-config'), + path('billing/account-payment-methods/', BillingAdminViewSet.as_view({'get': 'account_payment_methods', 'post': 'account_payment_methods'}), name='admin-billing-account-payment-methods'), + path('billing/account-payment-methods//', BillingAdminViewSet.as_view({ + 'get': 'account_payment_method', + 'patch': 'account_payment_method', + 'put': 'account_payment_method', + 'delete': 'account_payment_method', + }), name='admin-billing-account-payment-method'), + path('billing/account-payment-methods//set_default/', BillingAdminViewSet.as_view({'post': 'set_default_account_payment_method'}), name='admin-billing-account-payment-method-set-default'), ] urlpatterns += router.urls diff --git a/backend/seed_credit_packages.py b/backend/seed_credit_packages.py deleted file mode 100644 index 7c6f0bd4..00000000 --- a/backend/seed_credit_packages.py +++ /dev/null @@ -1,75 +0,0 @@ -""" -Seed credit packages for testing -""" -import os -import django - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'igny8_core.settings') -django.setup() - -from igny8_core.business.billing.models import CreditPackage -from decimal import Decimal - -def seed_credit_packages(): - """Create default credit packages""" - - packages = [ - { - 'name': 'Starter Pack', - 'slug': 'starter-pack', - 'credits': 1000, - 'price': Decimal('9.99'), - 'discount_percentage': 0, - 'description': 'Perfect for trying out the platform', - 'sort_order': 1, - 'is_featured': False - }, - { - 'name': 'Professional Pack', - 'slug': 'professional-pack', - 'credits': 5000, - 'price': Decimal('39.99'), - 'discount_percentage': 20, - 'description': 'Best for growing teams', - 'sort_order': 2, - 'is_featured': True - }, - { - 'name': 'Business Pack', - 'slug': 'business-pack', - 'credits': 15000, - 'price': Decimal('99.99'), - 'discount_percentage': 30, - 'description': 'Ideal for established businesses', - 'sort_order': 3, - 'is_featured': False - }, - { - 'name': 'Enterprise Pack', - 'slug': 'enterprise-pack', - 'credits': 50000, - 'price': Decimal('299.99'), - 'discount_percentage': 40, - 'description': 'Maximum value for high-volume users', - 'sort_order': 4, - 'is_featured': True - } - ] - - created_count = 0 - for pkg_data in packages: - pkg, created = CreditPackage.objects.get_or_create( - slug=pkg_data['slug'], - defaults=pkg_data - ) - if created: - created_count += 1 - print(f"āœ… Created: {pkg.name} - {pkg.credits:,} credits for ${pkg.price}") - else: - print(f"ā­ļø Exists: {pkg.name}") - - print(f"\nāœ… Seeded {created_count} new credit packages") - print(f"šŸ“Š Total active packages: {CreditPackage.objects.filter(is_active=True).count()}") - -if __name__ == '__main__': - seed_credit_packages() diff --git a/backend/seed_payment_configs.py b/backend/seed_payment_configs.py deleted file mode 100644 index ac2501d5..00000000 --- a/backend/seed_payment_configs.py +++ /dev/null @@ -1,125 +0,0 @@ -""" -Seed payment method configurations -""" -import os -import django - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'igny8_core.settings') -django.setup() - -from igny8_core.business.billing.models import PaymentMethodConfig - -def seed_payment_configs(): - """Create payment method configurations for various countries""" - - configs = [ - # United States - Stripe and PayPal only - { - 'country_code': 'US', - 'payment_method': 'stripe', - 'is_enabled': True, - 'display_name': 'Credit/Debit Card', - 'instructions': 'Pay securely with your credit or debit card via Stripe', - 'sort_order': 1 - }, - { - 'country_code': 'US', - 'payment_method': 'paypal', - 'is_enabled': True, - 'display_name': 'PayPal', - 'instructions': 'Pay with your PayPal account', - 'sort_order': 2 - }, - - # India - All methods including manual - { - 'country_code': 'IN', - 'payment_method': 'stripe', - 'is_enabled': True, - 'display_name': 'Credit/Debit Card', - 'instructions': 'Pay securely with your credit or debit card', - 'sort_order': 1 - }, - { - 'country_code': 'IN', - 'payment_method': 'paypal', - 'is_enabled': True, - 'display_name': 'PayPal', - 'instructions': 'Pay with your PayPal account', - 'sort_order': 2 - }, - { - 'country_code': 'IN', - 'payment_method': 'bank_transfer', - 'is_enabled': True, - 'display_name': 'Bank Transfer (NEFT/IMPS/RTGS)', - 'instructions': 'Transfer funds to our bank account. Payment will be verified within 1-2 business days.', - 'bank_name': 'HDFC Bank', - 'account_number': 'XXXXXXXXXXXXX', - 'routing_number': 'HDFC0000XXX', - 'swift_code': 'HDFCINBB', - 'sort_order': 3 - }, - { - 'country_code': 'IN', - 'payment_method': 'local_wallet', - 'is_enabled': True, - 'display_name': 'UPI / Digital Wallet', - 'instructions': 'Pay via Paytm, PhonePe, Google Pay, or other UPI apps. Upload payment screenshot for verification.', - 'wallet_type': 'UPI', - 'wallet_id': 'igny8@paytm', - 'sort_order': 4 - }, - - # United Kingdom - Stripe, PayPal, Bank Transfer - { - 'country_code': 'GB', - 'payment_method': 'stripe', - 'is_enabled': True, - 'display_name': 'Credit/Debit Card', - 'instructions': 'Pay securely with your credit or debit card', - 'sort_order': 1 - }, - { - 'country_code': 'GB', - 'payment_method': 'paypal', - 'is_enabled': True, - 'display_name': 'PayPal', - 'instructions': 'Pay with your PayPal account', - 'sort_order': 2 - }, - { - 'country_code': 'GB', - 'payment_method': 'bank_transfer', - 'is_enabled': True, - 'display_name': 'Bank Transfer (BACS/Faster Payments)', - 'instructions': 'Transfer funds to our UK bank account.', - 'bank_name': 'Barclays Bank', - 'account_number': 'XXXXXXXX', - 'routing_number': 'XX-XX-XX', - 'swift_code': 'BARCGB22', - 'sort_order': 3 - }, - ] - - created_count = 0 - updated_count = 0 - for config_data in configs: - config, created = PaymentMethodConfig.objects.update_or_create( - country_code=config_data['country_code'], - payment_method=config_data['payment_method'], - defaults={k: v for k, v in config_data.items() if k not in ['country_code', 'payment_method']} - ) - if created: - created_count += 1 - print(f"āœ… Created: {config.country_code} - {config.get_payment_method_display()}") - else: - updated_count += 1 - print(f"šŸ”„ Updated: {config.country_code} - {config.get_payment_method_display()}") - - print(f"\nāœ… Created {created_count} configurations") - print(f"šŸ”„ Updated {updated_count} configurations") - print(f"šŸ“Š Total active: {PaymentMethodConfig.objects.filter(is_enabled=True).count()}") - -if __name__ == '__main__': - seed_payment_configs() diff --git a/docs/API/API-COMPLETE-REFERENCE.md b/docs/API/API-COMPLETE-REFERENCE.md index 9c466e20..454962c2 100644 --- a/docs/API/API-COMPLETE-REFERENCE.md +++ b/docs/API/API-COMPLETE-REFERENCE.md @@ -1018,6 +1018,17 @@ class KeywordViewSet(SiteSectorModelViewSet): - `GET /api/v1/admin/billing/pending_payments/` - Pending manual payments (admin review queue) - `POST /api/v1/admin/billing/{id}/approve_payment/` - Approve manual payment (admin-only) - `POST /api/v1/admin/billing/{id}/reject_payment/` - Reject manual payment (admin-only) +- `GET /api/v1/admin/billing/payment-method-configs/` - List payment method configs (country-level); query: `country_code`, `payment_method` +- `POST /api/v1/admin/billing/payment-method-configs/` - Create payment method config +- `GET /api/v1/admin/billing/payment-method-configs/{id}/` - Retrieve payment method config +- `PATCH/PUT /api/v1/admin/billing/payment-method-configs/{id}/` - Update payment method config +- `DELETE /api/v1/admin/billing/payment-method-configs/{id}/` - Delete payment method config +- `GET /api/v1/admin/billing/account-payment-methods/` - List account payment methods (query `account_id` to scope) +- `POST /api/v1/admin/billing/account-payment-methods/` - Create account payment method +- `GET /api/v1/admin/billing/account-payment-methods/{id}/` - Retrieve account payment method +- `PATCH/PUT /api/v1/admin/billing/account-payment-methods/{id}/` - Update account payment method +- `DELETE /api/v1/admin/billing/account-payment-methods/{id}/` - Delete account payment method +- `POST /api/v1/admin/billing/account-payment-methods/{id}/set_default/` - Set default account payment method - `GET /api/v1/admin/credit-costs/` - List credit cost configurations (admin-only) - `POST /api/v1/admin/credit-costs/` - Update credit cost configurations (admin-only) - `GET /api/v1/admin/users/` - List users/accounts with credit info (admin-only) diff --git a/frontend/src/pages/account/PlansAndBillingPage.tsx b/frontend/src/pages/account/PlansAndBillingPage.tsx index c662f5ca..cde9de39 100644 --- a/frontend/src/pages/account/PlansAndBillingPage.tsx +++ b/frontend/src/pages/account/PlansAndBillingPage.tsx @@ -96,7 +96,7 @@ export default function PlansAndBillingPage() { setPackages(packagesData.results || []); setInvoices(invoicesData.results || []); setPayments(paymentsData.results || []); - const methods = methodsData.results || []; + const methods = (methodsData.results || []).filter((m) => m.is_enabled !== false); setPaymentMethods(methods); if (methods.length > 0) { const defaultMethod = methods.find((m) => m.is_default); @@ -209,7 +209,7 @@ export default function PlansAndBillingPage() { try { await createPaymentMethod(newPaymentMethod as any); toast?.success?.('Payment method added'); - setNewPaymentMethod({ type: 'bank_transfer', display_name: '', instructions: '' }); + setNewPaymentMethod({ type: 'bank_transfer', display_name: '', instructions: '' }); await loadData(); } catch (err: any) { handleBillingError(err, 'Failed to add payment method'); diff --git a/frontend/src/pages/admin/AdminAllPaymentsPage.tsx b/frontend/src/pages/admin/AdminAllPaymentsPage.tsx index 7a0845a1..fa793df0 100644 --- a/frontend/src/pages/admin/AdminAllPaymentsPage.tsx +++ b/frontend/src/pages/admin/AdminAllPaymentsPage.tsx @@ -1,31 +1,85 @@ /** - * Admin All Payments Page - * View and manage all payment transactions + * Admin Payments Page + * Tabs: All Payments, Pending Approvals (approve/reject), Payment Methods (country-level configs + per-account methods) */ -import { useState, useEffect } from 'react'; -import { Search, Filter, Loader2, AlertCircle } from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { Filter, Loader2, AlertCircle, Check, X, RefreshCw, Plus, Trash, Star } from 'lucide-react'; import { Card } from '../../components/ui/card'; import Badge from '../../components/ui/badge/Badge'; -import { getAdminPayments, type Payment } from '../../services/billing.api'; +import { + getAdminPayments, + getPendingPayments, + approvePayment, + rejectPayment, + getAdminPaymentMethodConfigs, + createAdminPaymentMethodConfig, + updateAdminPaymentMethodConfig, + deleteAdminPaymentMethodConfig, + getAdminAccountPaymentMethods, + createAdminAccountPaymentMethod, + updateAdminAccountPaymentMethod, + deleteAdminAccountPaymentMethod, + setAdminDefaultAccountPaymentMethod, + getAdminUsers, + type Payment, + type PaymentMethod, + type PaymentMethodConfig, + type AdminAccountPaymentMethod, + type AdminUser, +} from '../../services/billing.api'; type AdminPayment = Payment & { account_name?: string }; +type TabType = 'all' | 'pending' | 'methods'; export default function AdminAllPaymentsPage() { const [payments, setPayments] = useState([]); + const [pendingPayments, setPendingPayments] = useState([]); + const [paymentConfigs, setPaymentConfigs] = useState([]); + const [accounts, setAccounts] = useState([]); + const [accountPaymentMethods, setAccountPaymentMethods] = useState([]); + const [accountIdFilter, setAccountIdFilter] = useState(''); + const [selectedConfigIdForAccount, setSelectedConfigIdForAccount] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [statusFilter, setStatusFilter] = useState('all'); + const [activeTab, setActiveTab] = useState('all'); + const [actionLoadingId, setActionLoadingId] = useState(null); + const [rejectNotes, setRejectNotes] = useState>({}); + const [newConfig, setNewConfig] = useState<{ + country_code: string; + payment_method: PaymentMethod['type']; + display_name: string; + instructions?: string; + sort_order?: number; + is_enabled?: boolean; + }>({ + country_code: '*', + payment_method: 'bank_transfer', + display_name: '', + instructions: '', + sort_order: 0, + is_enabled: true, + }); + const [editingConfigId, setEditingConfigId] = useState(null); useEffect(() => { - loadPayments(); + loadAll(); }, []); - const loadPayments = async () => { + const loadAll = async () => { try { setLoading(true); - const data = await getAdminPayments(); - setPayments(data.results || []); + const [allData, pendingData, configsData, usersData] = await Promise.all([ + getAdminPayments(), + getPendingPayments(), + getAdminPaymentMethodConfigs(), + getAdminUsers(), + ]); + setPayments(allData.results || []); + setPendingPayments(pendingData.results || []); + setPaymentConfigs(configsData.results || []); + setAccounts(usersData.results || []); } catch (err: any) { setError(err.message || 'Failed to load payments'); } finally { @@ -33,9 +87,7 @@ export default function AdminAllPaymentsPage() { } }; - const filteredPayments = payments.filter((payment) => { - return statusFilter === 'all' || payment.status === statusFilter; - }); + const filteredPayments = payments.filter((payment) => statusFilter === 'all' || payment.status === statusFilter); const getStatusColor = (status: string) => { switch (status) { @@ -53,6 +105,212 @@ export default function AdminAllPaymentsPage() { } }; + const handleApprove = async (id: number) => { + try { + setActionLoadingId(id); + await approvePayment(id); + await loadAll(); + } catch (err: any) { + setError(err.message || 'Failed to approve payment'); + } finally { + setActionLoadingId(null); + } + }; + + const handleReject = async (id: number) => { + try { + setActionLoadingId(id); + await rejectPayment(id, { notes: rejectNotes[id] || '' }); + await loadAll(); + } catch (err: any) { + setError(err.message || 'Failed to reject payment'); + } finally { + setActionLoadingId(null); + } + }; + + // Payment method configs (country-level) + const handleSaveConfig = async () => { + if (!newConfig.display_name.trim()) { + setError('Payment method display name is required'); + return; + } + if (!newConfig.payment_method) { + setError('Payment method type is required'); + return; + } + try { + setActionLoadingId(-1); + if (editingConfigId) { + await updateAdminPaymentMethodConfig(editingConfigId, { + country_code: newConfig.country_code || '*', + payment_method: newConfig.payment_method, + display_name: newConfig.display_name, + instructions: newConfig.instructions, + sort_order: newConfig.sort_order, + is_enabled: newConfig.is_enabled ?? true, + }); + } else { + await createAdminPaymentMethodConfig({ + country_code: newConfig.country_code || '*', + payment_method: newConfig.payment_method, + display_name: newConfig.display_name, + instructions: newConfig.instructions, + sort_order: newConfig.sort_order, + is_enabled: newConfig.is_enabled ?? true, + }); + } + setNewConfig({ + country_code: '*', + payment_method: 'bank_transfer', + display_name: '', + instructions: '', + sort_order: 0, + is_enabled: true, + }); + setEditingConfigId(null); + const cfgs = await getAdminPaymentMethodConfigs(); + setPaymentConfigs(cfgs.results || []); + } catch (err: any) { + setError(err.message || 'Failed to add payment method config'); + } finally { + setActionLoadingId(null); + } + }; + + const handleToggleConfigEnabled = async (cfg: PaymentMethodConfig) => { + try { + setActionLoadingId(cfg.id); + await updateAdminPaymentMethodConfig(cfg.id, { is_enabled: !cfg.is_enabled }); + const cfgs = await getAdminPaymentMethodConfigs(); + setPaymentConfigs(cfgs.results || []); + } catch (err: any) { + setError(err.message || 'Failed to update payment method config'); + } finally { + setActionLoadingId(null); + } + }; + + const handleDeleteConfig = async (id: number) => { + try { + setActionLoadingId(id); + await deleteAdminPaymentMethodConfig(id); + const cfgs = await getAdminPaymentMethodConfigs(); + setPaymentConfigs(cfgs.results || []); + } catch (err: any) { + setError(err.message || 'Failed to delete payment method config'); + } finally { + setActionLoadingId(null); + } + }; + + const handleEditConfig = (cfg: PaymentMethodConfig) => { + setEditingConfigId(cfg.id); + setNewConfig({ + country_code: cfg.country_code, + payment_method: cfg.payment_method, + display_name: cfg.display_name, + instructions: cfg.instructions, + sort_order: cfg.sort_order, + is_enabled: cfg.is_enabled, + }); + }; + + const handleCancelConfigEdit = () => { + setEditingConfigId(null); + setNewConfig({ + country_code: '*', + payment_method: 'bank_transfer', + display_name: '', + instructions: '', + sort_order: 0, + is_enabled: true, + }); + }; + + // Account payment methods + const handleLoadAccountMethods = async () => { + const accountId = accountIdFilter.trim(); + if (!accountId) { + setAccountPaymentMethods([]); + return; + } + try { + setActionLoadingId(-3); + const data = await getAdminAccountPaymentMethods({ account_id: Number(accountId) }); + setAccountPaymentMethods(data.results || []); + } catch (err: any) { + setError(err.message || 'Failed to load account payment methods'); + } finally { + setActionLoadingId(null); + } + }; + + // Associate an existing country-level config to the account (one per account) + const handleAssociateConfigToAccount = async () => { + const accountId = accountIdFilter.trim(); + if (!accountId) { + setError('Select an account first'); + return; + } + if (!selectedConfigIdForAccount) { + setError('Select a payment method config to assign'); + return; + } + const cfg = paymentConfigs.find((c) => c.id === selectedConfigIdForAccount); + if (!cfg) { + setError('Selected config not found'); + return; + } + try { + setActionLoadingId(-2); + // Create or replace with the chosen config; treat as association. + const created = await createAdminAccountPaymentMethod({ + account: Number(accountId), + type: cfg.payment_method, + display_name: cfg.display_name, + instructions: cfg.instructions, + is_enabled: cfg.is_enabled, + is_default: true, + }); + // Remove extras if more than one exists for this account to enforce single association. + const refreshed = await getAdminAccountPaymentMethods({ account_id: Number(accountId) }); + const others = (refreshed.results || []).filter((m) => m.id !== created.id); + for (const other of others) { + await deleteAdminAccountPaymentMethod(other.id); + } + await handleLoadAccountMethods(); + } catch (err: any) { + setError(err.message || 'Failed to assign payment method to account'); + } finally { + setActionLoadingId(null); + } + }; + + const handleDeleteAccountMethod = async (id: number | string) => { + try { + setActionLoadingId(Number(id)); + await deleteAdminAccountPaymentMethod(id); + await handleLoadAccountMethods(); + } catch (err: any) { + setError(err.message || 'Failed to delete account payment method'); + } finally { + setActionLoadingId(null); + } + }; + + const handleSetDefaultAccountMethod = async (id: number | string) => { + try { + setActionLoadingId(Number(id)); + await setAdminDefaultAccountPaymentMethod(id); + await handleLoadAccountMethods(); + } catch (err: any) { + setError(err.message || 'Failed to set default account payment method'); + } finally { + setActionLoadingId(null); + } + }; + if (loading) { return (
@@ -61,13 +319,139 @@ export default function AdminAllPaymentsPage() { ); } + const renderPaymentsTable = (rows: AdminPayment[]) => ( + +
+ + + + + + + + + + + + + + {rows.length === 0 ? ( + + + + ) : ( + rows.map((payment) => ( + + + + + + + + + + )) + )} + +
AccountInvoiceAmountMethodStatusDateActions
No payments found
{payment.account_name} + {payment.invoice_number || payment.invoice_id || '—'} + {payment.currency} {payment.amount}{payment.payment_method.replace('_', ' ')} + + {payment.status} + + + {new Date(payment.created_at).toLocaleDateString()} + + +
+
+
+ ); + + const renderPendingTable = () => ( + +
+ + + + + + + + + + + + + {pendingPayments.length === 0 ? ( + + + + ) : ( + pendingPayments.map((payment) => ( + + + + + + + + + )) + )} + +
AccountInvoiceAmountMethodReferenceActions
No pending payments
{payment.account_name} + {payment.invoice_number || payment.invoice_id || '—'} + {payment.currency} {payment.amount}{payment.payment_method.replace('_', ' ')} + {payment.transaction_reference || '—'} + + +
+ setRejectNotes({ ...rejectNotes, [payment.id as number]: e.target.value })} + /> + +
+
+
+
+ ); + return (
-

All Payments

-

- View and manage all payment transactions -

+
+
+

Payments

+

+ Admin-only billing management +

+
+ +
{error && ( @@ -77,74 +461,293 @@ export default function AdminAllPaymentsPage() {
)} -
- - +
+
+ + + +
+ {activeTab === 'all' && ( +
+ + +
+ )}
- -
- - - - - - - - - - - - - - {filteredPayments.length === 0 ? ( - - - - ) : ( - filteredPayments.map((payment) => ( - - - - - - - - - - )) + {activeTab === 'all' && renderPaymentsTable(filteredPayments)} + {activeTab === 'pending' && renderPendingTable()} + {activeTab === 'methods' && ( +
+ {/* Payment Method Configs (country-level) */} + +

Payment Method Configs (country-level)

+
+ setNewConfig({ ...newConfig, country_code: e.target.value })} + /> + + setNewConfig({ ...newConfig, display_name: e.target.value })} + /> + setNewConfig({ ...newConfig, instructions: e.target.value })} + /> + setNewConfig({ ...newConfig, sort_order: Number(e.target.value) })} + /> + + + {editingConfigId && ( + )} -
-
AccountInvoiceAmountMethodStatusDateActions
No payments found
{payment.account_name} - {payment.invoice_number || payment.invoice_id || '—'} - {payment.currency} {payment.amount}{payment.payment_method.replace('_', ' ')} - - {payment.status} - - - {new Date(payment.created_at).toLocaleDateString()} - - -
+
+
+ + +
+ + + + + + + + + + + + + {paymentConfigs.length === 0 ? ( + + + + ) : ( + paymentConfigs.map((cfg) => ( + + + + + + + + + )) + )} + +
CountryNameTypeEnabledInstructionsActions
No payment method configs
{cfg.country_code}{cfg.display_name}{cfg.payment_method.replace('_', ' ')} + + + + {cfg.instructions || '—'} + + +
+
+
+ + {/* Account Payment Methods (associate existing configs only) */} + +
+

Account Payment Methods (association)

+
+ + +
+
+ +
+ + +

+ Only one payment method per account; assigning replaces existing. +

+
+ +
+ + + + + + + + + + + + + + {accountPaymentMethods.length === 0 ? ( + + + + ) : ( + accountPaymentMethods.map((m) => ( + + + + + + + + + + )) + )} + +
AccountNameTypeEnabledDefaultInstructionsActions
No account payment methods
{m.account}{m.display_name}{m.type.replace('_', ' ')}{m.is_enabled ? 'Yes' : 'No'}{m.is_default ? : '—'} + {m.instructions || '—'} + + {!m.is_default && ( + + )} + +
+
+
- + )}
); } diff --git a/frontend/src/pages/admin/AdminSystemDashboard.tsx b/frontend/src/pages/admin/AdminSystemDashboard.tsx index 3835680c..8963cbfa 100644 --- a/frontend/src/pages/admin/AdminSystemDashboard.tsx +++ b/frontend/src/pages/admin/AdminSystemDashboard.tsx @@ -4,10 +4,21 @@ */ import { useState, useEffect } from 'react'; -import { - Users, DollarSign, TrendingUp, AlertCircle, - CheckCircle, Clock, Activity, Loader2, ExternalLink, - Globe, Database, Folder, Server, GitBranch, FileText +import { + Users, + CheckCircle, + DollarSign, + Clock, + AlertCircle, + Activity, + Loader2, + ExternalLink, + Globe, + Database, + Folder, + Server, + GitBranch, + FileText, } from 'lucide-react'; import { Card } from '../../components/ui/card'; import Badge from '../../components/ui/badge/Badge'; @@ -17,8 +28,11 @@ export default function AdminSystemDashboard() { const [loading, setLoading] = useState(true); const [stats, setStats] = useState(null); const [error, setError] = useState(''); - const issuedCredits = Number(stats?.credits_issued_30d || 0); - const usedCredits = Number(stats?.credits_used_30d || 0); + + const totalUsers = Number(stats?.total_users ?? 0); + const activeUsers = Number(stats?.active_users ?? 0); + const issuedCredits = Number(stats?.total_credits_issued ?? stats?.credits_issued_30d ?? 0); + const usedCredits = Number(stats?.total_credits_used ?? stats?.credits_used_30d ?? 0); const creditScale = Math.max(issuedCredits, usedCredits, 1); const issuedPct = Math.min(100, Math.round((issuedCredits / creditScale) * 100)); const usedPct = Math.min(100, Math.round((usedCredits / creditScale) * 100)); @@ -65,7 +79,7 @@ export default function AdminSystemDashboard() {

System Dashboard

- Overview of system health, accounts, and revenue + Overview of system health and billing activity

@@ -81,12 +95,9 @@ export default function AdminSystemDashboard() {
-
Total Accounts
+
Total Users
- {stats?.total_accounts?.toLocaleString() || 0} -
-
- +{stats?.new_accounts_this_month || 0} this month + {totalUsers.toLocaleString()}
@@ -96,11 +107,10 @@ export default function AdminSystemDashboard() {
-
Active Subscriptions
+
Active Users
- {stats?.active_subscriptions?.toLocaleString() || 0} + {activeUsers.toLocaleString()}
-
paying customers
@@ -109,13 +119,11 @@ export default function AdminSystemDashboard() {
-
Revenue This Month
+
Credits Issued
- ${Number(stats?.revenue_this_month || 0).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} -
-
- +12% vs last month + {issuedCredits.toLocaleString()}
+
lifetime total
@@ -124,11 +132,11 @@ export default function AdminSystemDashboard() {
-
Pending Approvals
+
Credits Used
- {stats?.pending_approvals || 0} + {usedCredits.toLocaleString()}
-
requires attention
+
lifetime total
@@ -219,46 +227,6 @@ export default function AdminSystemDashboard() {
- {/* Recent Activity */} - -

Recent Activity

-
- - - - - - - - - - - - {stats?.recent_activity?.map((activity: any, idx: number) => ( - - - - - - - - )) || ( - - - - )} - -
TypeAccountDescriptionAmountTime
- - {activity.type} - - {activity.account_name}{activity.description} - {activity.currency} {activity.amount} - - {new Date(activity.timestamp).toLocaleTimeString()} -
No recent activity
-
-
); } diff --git a/frontend/src/services/billing.api.ts b/frontend/src/services/billing.api.ts index 922d4226..13f688a2 100644 --- a/frontend/src/services/billing.api.ts +++ b/frontend/src/services/billing.api.ts @@ -162,12 +162,13 @@ export interface TeamMember { export interface PaymentMethod { id: string; - type: 'stripe' | 'paypal' | 'bank_transfer' | 'local_wallet'; + type: 'stripe' | 'paypal' | 'bank_transfer' | 'local_wallet' | 'manual'; display_name: string; name?: string; is_enabled: boolean; is_default?: boolean; instructions?: string; + country_code?: string; bank_details?: { bank_name?: string; account_number?: string; @@ -297,6 +298,90 @@ export async function getAdminPayments(params?: { status?: string; account_id?: return fetchAPI(url); } +// Admin payment method configs (country-level) +export interface PaymentMethodConfig { + id: number; + country_code: string; + payment_method: PaymentMethod['type']; + display_name: string; + is_enabled: boolean; + instructions?: string; + sort_order?: number; +} + +export async function getAdminPaymentMethodConfigs(params?: { country_code?: string; payment_method?: string }): Promise<{ + results: PaymentMethodConfig[]; + count: number; +}> { + const queryParams = new URLSearchParams(); + if (params?.country_code) queryParams.append('country_code', params.country_code); + if (params?.payment_method) queryParams.append('payment_method', params.payment_method); + const url = `/v1/admin/billing/payment-method-configs/${queryParams.toString() ? '?' + queryParams.toString() : ''}`; + return fetchAPI(url); +} + +export async function createAdminPaymentMethodConfig(data: Partial): Promise { + return fetchAPI('/v1/admin/billing/payment-method-configs/', { + method: 'POST', + body: JSON.stringify(data), + }); +} + +export async function updateAdminPaymentMethodConfig(id: number, data: Partial): Promise { + return fetchAPI(`/v1/admin/billing/payment-method-configs/${id}/`, { + method: 'PATCH', + body: JSON.stringify(data), + }); +} + +export async function deleteAdminPaymentMethodConfig(id: number): Promise { + await fetchAPI(`/v1/admin/billing/payment-method-configs/${id}/`, { + method: 'DELETE', + }); +} + +// Admin account payment methods (cross-account) +export interface AdminAccountPaymentMethod extends PaymentMethod { + account: number; + metadata?: Record; +} + +export async function getAdminAccountPaymentMethods(params?: { account_id?: number }): Promise<{ + results: AdminAccountPaymentMethod[]; + count: number; +}> { + const queryParams = new URLSearchParams(); + if (params?.account_id) queryParams.append('account_id', String(params.account_id)); + const url = `/v1/admin/billing/account-payment-methods/${queryParams.toString() ? '?' + queryParams.toString() : ''}`; + return fetchAPI(url); +} + +export async function createAdminAccountPaymentMethod(data: Partial): Promise { + return fetchAPI('/v1/admin/billing/account-payment-methods/', { + method: 'POST', + body: JSON.stringify(data), + }); +} + +export async function updateAdminAccountPaymentMethod(id: number | string, data: Partial): Promise { + return fetchAPI(`/v1/admin/billing/account-payment-methods/${id}/`, { + method: 'PATCH', + body: JSON.stringify(data), + }); +} + +export async function deleteAdminAccountPaymentMethod(id: number | string): Promise { + await fetchAPI(`/v1/admin/billing/account-payment-methods/${id}/`, { + method: 'DELETE', + }); +} + +export async function setAdminDefaultAccountPaymentMethod(id: number | string): Promise<{ message: string; id: number | string }> { + return fetchAPI(`/v1/admin/billing/account-payment-methods/${id}/set_default/`, { + method: 'POST', + }); +} + export async function getAdminUsers(params?: { search?: string; role?: string;