billing and paymetn methods
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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']
|
||||
|
||||
|
||||
|
||||
@@ -19,6 +19,21 @@ urlpatterns = [
|
||||
path('billing/pending_payments/', BillingAdminViewSet.as_view({'get': 'pending_payments'}), name='admin-billing-pending-payments'),
|
||||
path('billing/<int:pk>/approve_payment/', BillingAdminViewSet.as_view({'post': 'approve_payment'}), name='admin-billing-approve-payment'),
|
||||
path('billing/<int:pk>/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/<int:pk>/', 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/<int:pk>/', 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/<int:pk>/set_default/', BillingAdminViewSet.as_view({'post': 'set_default_account_payment_method'}), name='admin-billing-account-payment-method-set-default'),
|
||||
]
|
||||
|
||||
urlpatterns += router.urls
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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<AdminPayment[]>([]);
|
||||
const [pendingPayments, setPendingPayments] = useState<AdminPayment[]>([]);
|
||||
const [paymentConfigs, setPaymentConfigs] = useState<PaymentMethodConfig[]>([]);
|
||||
const [accounts, setAccounts] = useState<AdminUser[]>([]);
|
||||
const [accountPaymentMethods, setAccountPaymentMethods] = useState<AdminAccountPaymentMethod[]>([]);
|
||||
const [accountIdFilter, setAccountIdFilter] = useState<string>('');
|
||||
const [selectedConfigIdForAccount, setSelectedConfigIdForAccount] = useState<number | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [statusFilter, setStatusFilter] = useState('all');
|
||||
const [activeTab, setActiveTab] = useState<TabType>('all');
|
||||
const [actionLoadingId, setActionLoadingId] = useState<number | null>(null);
|
||||
const [rejectNotes, setRejectNotes] = useState<Record<number, string>>({});
|
||||
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<number | null>(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 (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
@@ -61,13 +319,139 @@ export default function AdminAllPaymentsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const renderPaymentsTable = (rows: AdminPayment[]) => (
|
||||
<Card className="overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<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>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Date</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{rows.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="px-6 py-8 text-center text-gray-500">No payments found</td>
|
||||
</tr>
|
||||
) : (
|
||||
rows.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 capitalize">{payment.payment_method.replace('_', ' ')}</td>
|
||||
<td className="px-6 py-4">
|
||||
<Badge variant="light" color={getStatusColor(payment.status)}>
|
||||
{payment.status}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-600">
|
||||
{new Date(payment.created_at).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<button className="text-blue-600 hover:text-blue-700 text-sm">View</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
const renderPendingTable = () => (
|
||||
<Card className="overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<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">Reference</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{pendingPayments.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="px-6 py-8 text-center text-gray-500">No pending payments</td>
|
||||
</tr>
|
||||
) : (
|
||||
pendingPayments.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 capitalize">{payment.payment_method.replace('_', ' ')}</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-700 dark:text-gray-300">
|
||||
{payment.transaction_reference || '—'}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right flex items-center justify-end gap-2">
|
||||
<button
|
||||
className="inline-flex items-center gap-1 text-green-600 hover:text-green-700 text-sm px-2 py-1 border border-green-200 rounded"
|
||||
disabled={actionLoadingId === payment.id}
|
||||
onClick={() => handleApprove(payment.id as number)}
|
||||
>
|
||||
{actionLoadingId === payment.id ? <Loader2 className="w-4 h-4 animate-spin" /> : <Check className="w-4 h-4" />}
|
||||
Approve
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
className="text-sm px-2 py-1 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800"
|
||||
placeholder="Rejection notes"
|
||||
value={rejectNotes[payment.id as number] || ''}
|
||||
onChange={(e) => setRejectNotes({ ...rejectNotes, [payment.id as number]: e.target.value })}
|
||||
/>
|
||||
<button
|
||||
className="inline-flex items-center gap-1 text-red-600 hover:text-red-700 text-sm px-2 py-1 border border-red-200 rounded"
|
||||
disabled={actionLoadingId === payment.id}
|
||||
onClick={() => handleReject(payment.id as number)}
|
||||
>
|
||||
{actionLoadingId === payment.id ? <Loader2 className="w-4 h-4 animate-spin" /> : <X className="w-4 h-4" />}
|
||||
Reject
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">All Payments</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
View and manage all payment transactions
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Payments</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Admin-only billing management
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={loadAll}
|
||||
className="inline-flex items-center gap-2 px-3 py-2 text-sm border border-gray-300 dark:border-gray-700 rounded hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
@@ -77,74 +461,293 @@ export default function AdminAllPaymentsPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-6 flex items-center gap-2">
|
||||
<Filter className="w-5 h-5 text-gray-400" />
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
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 className="mb-4 flex items-center gap-4">
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
className={`px-3 py-2 rounded-lg text-sm border ${activeTab === 'all' ? 'border-blue-500 text-blue-600' : 'border-gray-300 dark:border-gray-700 text-gray-600 dark:text-gray-300'}`}
|
||||
onClick={() => setActiveTab('all')}
|
||||
>
|
||||
All Payments
|
||||
</button>
|
||||
<button
|
||||
className={`px-3 py-2 rounded-lg text-sm border ${activeTab === 'pending' ? 'border-blue-500 text-blue-600' : 'border-gray-300 dark:border-gray-700 text-gray-600 dark:text-gray-300'}`}
|
||||
onClick={() => setActiveTab('pending')}
|
||||
>
|
||||
Pending Approvals
|
||||
</button>
|
||||
<button
|
||||
className={`px-3 py-2 rounded-lg text-sm border ${activeTab === 'methods' ? 'border-blue-500 text-blue-600' : 'border-gray-300 dark:border-gray-700 text-gray-600 dark:text-gray-300'}`}
|
||||
onClick={() => setActiveTab('methods')}
|
||||
>
|
||||
Payment Methods
|
||||
</button>
|
||||
</div>
|
||||
{activeTab === 'all' && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Filter className="w-5 h-5 text-gray-400" />
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Card className="overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<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>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Date</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{filteredPayments.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="px-6 py-8 text-center text-gray-500">No payments found</td>
|
||||
</tr>
|
||||
) : (
|
||||
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 capitalize">{payment.payment_method.replace('_', ' ')}</td>
|
||||
<td className="px-6 py-4">
|
||||
<Badge
|
||||
variant="light"
|
||||
color={getStatusColor(payment.status)}
|
||||
>
|
||||
{payment.status}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-600">
|
||||
{new Date(payment.created_at).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<button className="text-blue-600 hover:text-blue-700 text-sm">View</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
{activeTab === 'all' && renderPaymentsTable(filteredPayments)}
|
||||
{activeTab === 'pending' && renderPendingTable()}
|
||||
{activeTab === 'methods' && (
|
||||
<div className="space-y-6">
|
||||
{/* Payment Method Configs (country-level) */}
|
||||
<Card className="p-4">
|
||||
<h3 className="text-lg font-semibold mb-3">Payment Method Configs (country-level)</h3>
|
||||
<div className="flex flex-col lg:flex-row gap-3">
|
||||
<input
|
||||
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded w-32"
|
||||
placeholder="Country (e.g., *, US)"
|
||||
value={newConfig.country_code}
|
||||
onChange={(e) => setNewConfig({ ...newConfig, country_code: e.target.value })}
|
||||
/>
|
||||
<select
|
||||
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded w-40"
|
||||
value={newConfig.payment_method}
|
||||
onChange={(e) => setNewConfig({ ...newConfig, payment_method: e.target.value as PaymentMethod['type'] })}
|
||||
>
|
||||
<option value="bank_transfer">Bank Transfer</option>
|
||||
<option value="local_wallet">Manual (local wallet)</option>
|
||||
<option value="stripe">Stripe</option>
|
||||
<option value="paypal">PayPal</option>
|
||||
</select>
|
||||
<input
|
||||
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded"
|
||||
placeholder="Display name"
|
||||
value={newConfig.display_name}
|
||||
onChange={(e) => setNewConfig({ ...newConfig, display_name: e.target.value })}
|
||||
/>
|
||||
<input
|
||||
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded"
|
||||
placeholder="Instructions (optional)"
|
||||
value={newConfig.instructions || ''}
|
||||
onChange={(e) => setNewConfig({ ...newConfig, instructions: e.target.value })}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
className="w-28 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded"
|
||||
placeholder="Sort"
|
||||
value={newConfig.sort_order ?? 0}
|
||||
onChange={(e) => setNewConfig({ ...newConfig, sort_order: Number(e.target.value) })}
|
||||
/>
|
||||
<label className="inline-flex items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!!newConfig.is_enabled}
|
||||
onChange={(e) => setNewConfig({ ...newConfig, is_enabled: e.target.checked })}
|
||||
/>
|
||||
Enabled
|
||||
</label>
|
||||
<button
|
||||
className="inline-flex items-center gap-2 px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
|
||||
onClick={handleSaveConfig}
|
||||
disabled={actionLoadingId === -1}
|
||||
>
|
||||
{actionLoadingId === -1 ? <Loader2 className="w-4 h-4 animate-spin" /> : <Plus className="w-4 h-4" />}
|
||||
{editingConfigId ? 'Save' : 'Add'}
|
||||
</button>
|
||||
{editingConfigId && (
|
||||
<button
|
||||
className="inline-flex items-center gap-2 px-3 py-2 border border-gray-300 rounded hover:bg-gray-50 disabled:opacity-50"
|
||||
onClick={handleCancelConfigEdit}
|
||||
disabled={actionLoadingId === -1}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<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">Country</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Name</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Type</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Enabled</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Instructions</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{paymentConfigs.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="px-6 py-8 text-center text-gray-500">No payment method configs</td>
|
||||
</tr>
|
||||
) : (
|
||||
paymentConfigs.map((cfg) => (
|
||||
<tr key={cfg.id} className="hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<td className="px-6 py-4 font-medium">{cfg.country_code}</td>
|
||||
<td className="px-6 py-4 font-medium">{cfg.display_name}</td>
|
||||
<td className="px-6 py-4 text-sm capitalize">{cfg.payment_method.replace('_', ' ')}</td>
|
||||
<td className="px-6 py-4 text-sm">
|
||||
<button
|
||||
className="text-blue-600 hover:text-blue-700 text-sm mr-3"
|
||||
onClick={() => handleToggleConfigEnabled(cfg)}
|
||||
disabled={actionLoadingId === cfg.id}
|
||||
>
|
||||
{actionLoadingId === cfg.id ? <Loader2 className="w-4 h-4 animate-spin" /> : cfg.is_enabled ? 'Disable' : 'Enable'}
|
||||
</button>
|
||||
<button
|
||||
className="text-blue-600 hover:text-blue-700 text-sm"
|
||||
onClick={() => handleEditConfig(cfg)}
|
||||
disabled={actionLoadingId === cfg.id}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-700 dark:text-gray-300">
|
||||
{cfg.instructions || '—'}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<button
|
||||
className="inline-flex items-center gap-1 text-red-600 hover:text-red-700 text-sm px-2 py-1 border border-red-200 rounded"
|
||||
onClick={() => handleDeleteConfig(cfg.id)}
|
||||
disabled={actionLoadingId === cfg.id}
|
||||
>
|
||||
{actionLoadingId === cfg.id ? <Loader2 className="w-4 h-4 animate-spin" /> : <Trash className="w-4 h-4" />}
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Account Payment Methods (associate existing configs only) */}
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-lg font-semibold">Account Payment Methods (association)</h3>
|
||||
<div className="flex items-center gap-3">
|
||||
<select
|
||||
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded w-52"
|
||||
value={accountIdFilter}
|
||||
onChange={(e) => setAccountIdFilter(e.target.value)}
|
||||
>
|
||||
<option value="">Select account</option>
|
||||
{accounts.map((acc) => (
|
||||
<option key={acc.id} value={acc.id}>
|
||||
{acc.account_name || acc.email || acc.id}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
className="inline-flex items-center gap-2 px-3 py-2 border border-gray-300 rounded hover:bg-gray-50 disabled:opacity-50"
|
||||
onClick={handleLoadAccountMethods}
|
||||
disabled={!accountIdFilter}
|
||||
>
|
||||
{actionLoadingId === -3 ? <Loader2 className="w-4 h-4 animate-spin" /> : <RefreshCw className="w-4 h-4" />}
|
||||
Load
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-3 mb-4">
|
||||
<select
|
||||
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded w-64"
|
||||
value={selectedConfigIdForAccount ?? ''}
|
||||
onChange={(e) => setSelectedConfigIdForAccount(e.target.value ? Number(e.target.value) : null)}
|
||||
disabled={!accountIdFilter}
|
||||
>
|
||||
<option value="">Select payment method config</option>
|
||||
{paymentConfigs.map((cfg) => (
|
||||
<option key={cfg.id} value={cfg.id}>
|
||||
{cfg.display_name} ({cfg.payment_method.replace('_', ' ')} - {cfg.country_code})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
className="inline-flex items-center gap-2 px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
|
||||
onClick={handleAssociateConfigToAccount}
|
||||
disabled={!accountIdFilter || !selectedConfigIdForAccount || actionLoadingId === -2}
|
||||
>
|
||||
{actionLoadingId === -2 ? <Loader2 className="w-4 h-4 animate-spin" /> : <Plus className="w-4 h-4" />}
|
||||
Assign to account
|
||||
</button>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Only one payment method per account; assigning replaces existing.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<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">Name</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Type</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Enabled</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Default</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Instructions</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{accountPaymentMethods.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="px-6 py-8 text-center text-gray-500">No account payment methods</td>
|
||||
</tr>
|
||||
) : (
|
||||
accountPaymentMethods.map((m) => (
|
||||
<tr key={m.id} className="hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<td className="px-6 py-4 font-medium">{m.account}</td>
|
||||
<td className="px-6 py-4 font-medium">{m.display_name}</td>
|
||||
<td className="px-6 py-4 text-sm capitalize">{m.type.replace('_', ' ')}</td>
|
||||
<td className="px-6 py-4 text-sm">{m.is_enabled ? 'Yes' : 'No'}</td>
|
||||
<td className="px-6 py-4 text-sm">{m.is_default ? <Star className="w-4 h-4 text-yellow-500" /> : '—'}</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-700 dark:text-gray-300">
|
||||
{m.instructions || '—'}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right space-x-2">
|
||||
{!m.is_default && (
|
||||
<button
|
||||
className="text-blue-600 hover:text-blue-700 text-sm"
|
||||
onClick={() => handleSetDefaultAccountMethod(m.id)}
|
||||
disabled={actionLoadingId === Number(m.id)}
|
||||
>
|
||||
{actionLoadingId === Number(m.id) ? <Loader2 className="w-4 h-4 animate-spin inline" /> : 'Set default'}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="inline-flex items-center gap-1 text-red-600 hover:text-red-700 text-sm px-2 py-1 border border-red-200 rounded"
|
||||
onClick={() => handleDeleteAccountMethod(m.id)}
|
||||
disabled={actionLoadingId === Number(m.id)}
|
||||
>
|
||||
{actionLoadingId === Number(m.id) ? <Loader2 className="w-4 h-4 animate-spin" /> : <Trash className="w-4 h-4" />}
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<any>(null);
|
||||
const [error, setError] = useState<string>('');
|
||||
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() {
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">System Dashboard</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Overview of system health, accounts, and revenue
|
||||
Overview of system health and billing activity
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -81,12 +95,9 @@ export default function AdminSystemDashboard() {
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Total Accounts</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Total Users</div>
|
||||
<div className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{stats?.total_accounts?.toLocaleString() || 0}
|
||||
</div>
|
||||
<div className="text-sm text-green-600 mt-1">
|
||||
+{stats?.new_accounts_this_month || 0} this month
|
||||
{totalUsers.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<Users className="w-12 h-12 text-blue-600 opacity-50" />
|
||||
@@ -96,11 +107,10 @@ export default function AdminSystemDashboard() {
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Active Subscriptions</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Active Users</div>
|
||||
<div className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{stats?.active_subscriptions?.toLocaleString() || 0}
|
||||
{activeUsers.toLocaleString()}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 mt-1">paying customers</div>
|
||||
</div>
|
||||
<CheckCircle className="w-12 h-12 text-green-600 opacity-50" />
|
||||
</div>
|
||||
@@ -109,13 +119,11 @@ export default function AdminSystemDashboard() {
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Revenue This Month</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Credits Issued</div>
|
||||
<div className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
${Number(stats?.revenue_this_month || 0).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
||||
</div>
|
||||
<div className="text-sm text-green-600 mt-1">
|
||||
<TrendingUp className="w-4 h-4 inline" /> +12% vs last month
|
||||
{issuedCredits.toLocaleString()}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 mt-1">lifetime total</div>
|
||||
</div>
|
||||
<DollarSign className="w-12 h-12 text-green-600 opacity-50" />
|
||||
</div>
|
||||
@@ -124,11 +132,11 @@ export default function AdminSystemDashboard() {
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Pending Approvals</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mb-1">Credits Used</div>
|
||||
<div className="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{stats?.pending_approvals || 0}
|
||||
{usedCredits.toLocaleString()}
|
||||
</div>
|
||||
<div className="text-sm text-yellow-600 mt-1">requires attention</div>
|
||||
<div className="text-sm text-gray-500 mt-1">lifetime total</div>
|
||||
</div>
|
||||
<Clock className="w-12 h-12 text-yellow-600 opacity-50" />
|
||||
</div>
|
||||
@@ -219,46 +227,6 @@ export default function AdminSystemDashboard() {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Recent Activity */}
|
||||
<Card className="p-6">
|
||||
<h2 className="text-lg font-semibold mb-4">Recent Activity</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="border-b border-gray-200 dark:border-gray-700">
|
||||
<tr>
|
||||
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Type</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Account</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Description</th>
|
||||
<th className="text-right py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Amount</th>
|
||||
<th className="text-right py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100 dark:divide-gray-800">
|
||||
{stats?.recent_activity?.map((activity: any, idx: number) => (
|
||||
<tr key={idx} className="hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<td className="py-3 px-4">
|
||||
<Badge variant="light" color={activity.type === 'purchase' ? 'success' : 'primary'}>
|
||||
{activity.type}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="py-3 px-4 text-sm text-gray-900 dark:text-white">{activity.account_name}</td>
|
||||
<td className="py-3 px-4 text-sm text-gray-600 dark:text-gray-400">{activity.description}</td>
|
||||
<td className="py-3 px-4 text-sm text-right font-semibold">
|
||||
{activity.currency} {activity.amount}
|
||||
</td>
|
||||
<td className="py-3 px-4 text-sm text-right text-gray-500">
|
||||
{new Date(activity.timestamp).toLocaleTimeString()}
|
||||
</td>
|
||||
</tr>
|
||||
)) || (
|
||||
<tr>
|
||||
<td colSpan={5} className="py-8 text-center text-gray-500">No recent activity</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<PaymentMethodConfig>): Promise<PaymentMethodConfig> {
|
||||
return fetchAPI('/v1/admin/billing/payment-method-configs/', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateAdminPaymentMethodConfig(id: number, data: Partial<PaymentMethodConfig>): Promise<PaymentMethodConfig> {
|
||||
return fetchAPI(`/v1/admin/billing/payment-method-configs/${id}/`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteAdminPaymentMethodConfig(id: number): Promise<void> {
|
||||
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<string, any>;
|
||||
}
|
||||
|
||||
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<AdminAccountPaymentMethod>): Promise<AdminAccountPaymentMethod> {
|
||||
return fetchAPI('/v1/admin/billing/account-payment-methods/', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateAdminAccountPaymentMethod(id: number | string, data: Partial<AdminAccountPaymentMethod>): Promise<AdminAccountPaymentMethod> {
|
||||
return fetchAPI(`/v1/admin/billing/account-payment-methods/${id}/`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteAdminAccountPaymentMethod(id: number | string): Promise<void> {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user