adsasdasd
This commit is contained in:
@@ -21,6 +21,21 @@ class AccountModelViewSet(viewsets.ModelViewSet):
|
||||
user = getattr(self.request, 'user', None)
|
||||
|
||||
if user and hasattr(user, 'is_authenticated') and user.is_authenticated:
|
||||
# Bypass filtering for superusers - they can see everything
|
||||
if getattr(user, 'is_superuser', False):
|
||||
return queryset
|
||||
|
||||
# Bypass filtering for developers
|
||||
if hasattr(user, 'role') and user.role == 'developer':
|
||||
return queryset
|
||||
|
||||
# Bypass filtering for system account users
|
||||
try:
|
||||
if hasattr(user, 'is_system_account_user') and user.is_system_account_user():
|
||||
return queryset
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
account = getattr(self.request, 'account', None)
|
||||
if not account and hasattr(self.request, 'user') and self.request.user and hasattr(self.request.user, 'is_authenticated') and self.request.user.is_authenticated:
|
||||
@@ -239,6 +254,29 @@ class SiteSectorModelViewSet(AccountModelViewSet):
|
||||
|
||||
# Check if user is authenticated and is a proper User instance (not AnonymousUser)
|
||||
if user and hasattr(user, 'is_authenticated') and user.is_authenticated and hasattr(user, 'get_accessible_sites'):
|
||||
# Bypass site filtering for superusers and developers
|
||||
# They already got unfiltered queryset from parent AccountModelViewSet
|
||||
if getattr(user, 'is_superuser', False) or (hasattr(user, 'role') and user.role == 'developer'):
|
||||
# No site filtering for superuser/developer
|
||||
# But still apply query param filters if provided
|
||||
try:
|
||||
query_params = getattr(self.request, 'query_params', None)
|
||||
if query_params is None:
|
||||
query_params = getattr(self.request, 'GET', {})
|
||||
site_id = query_params.get('site_id') or query_params.get('site')
|
||||
except AttributeError:
|
||||
site_id = None
|
||||
|
||||
if site_id:
|
||||
try:
|
||||
site_id_int = int(site_id) if site_id else None
|
||||
if site_id_int:
|
||||
queryset = queryset.filter(site_id=site_id_int)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
return queryset
|
||||
|
||||
try:
|
||||
# Get user's accessible sites
|
||||
accessible_sites = user.get_accessible_sites()
|
||||
|
||||
@@ -26,11 +26,27 @@ class HasTenantAccess(permissions.BasePermission):
|
||||
"""
|
||||
Permission class that requires user to belong to the tenant/account
|
||||
Ensures tenant isolation
|
||||
Superusers, developers, and system account users bypass this check.
|
||||
"""
|
||||
def has_permission(self, request, view):
|
||||
if not request.user or not request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
# Bypass for superusers
|
||||
if getattr(request.user, 'is_superuser', False):
|
||||
return True
|
||||
|
||||
# Bypass for developers
|
||||
if hasattr(request.user, 'role') and request.user.role == 'developer':
|
||||
return True
|
||||
|
||||
# Bypass for system account users
|
||||
try:
|
||||
if hasattr(request.user, 'is_system_account_user') and request.user.is_system_account_user():
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Get account from request (set by middleware)
|
||||
account = getattr(request, 'account', None)
|
||||
|
||||
@@ -58,11 +74,20 @@ class IsViewerOrAbove(permissions.BasePermission):
|
||||
"""
|
||||
Permission class that requires viewer, editor, admin, or owner role
|
||||
For read-only operations
|
||||
Superusers and developers bypass this check.
|
||||
"""
|
||||
def has_permission(self, request, view):
|
||||
if not request.user or not request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
# Bypass for superusers
|
||||
if getattr(request.user, 'is_superuser', False):
|
||||
return True
|
||||
|
||||
# Bypass for developers
|
||||
if hasattr(request.user, 'role') and request.user.role == 'developer':
|
||||
return True
|
||||
|
||||
# Check user role
|
||||
if hasattr(request.user, 'role'):
|
||||
role = request.user.role
|
||||
@@ -77,11 +102,20 @@ class IsEditorOrAbove(permissions.BasePermission):
|
||||
"""
|
||||
Permission class that requires editor, admin, or owner role
|
||||
For content operations
|
||||
Superusers and developers bypass this check.
|
||||
"""
|
||||
def has_permission(self, request, view):
|
||||
if not request.user or not request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
# Bypass for superusers
|
||||
if getattr(request.user, 'is_superuser', False):
|
||||
return True
|
||||
|
||||
# Bypass for developers
|
||||
if hasattr(request.user, 'role') and request.user.role == 'developer':
|
||||
return True
|
||||
|
||||
# Check user role
|
||||
if hasattr(request.user, 'role'):
|
||||
role = request.user.role
|
||||
@@ -96,11 +130,20 @@ class IsAdminOrOwner(permissions.BasePermission):
|
||||
"""
|
||||
Permission class that requires admin or owner role only
|
||||
For settings, keys, billing operations
|
||||
Superusers and developers bypass this check.
|
||||
"""
|
||||
def has_permission(self, request, view):
|
||||
if not request.user or not request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
# Bypass for superusers
|
||||
if getattr(request.user, 'is_superuser', False):
|
||||
return True
|
||||
|
||||
# Bypass for developers
|
||||
if hasattr(request.user, 'role') and request.user.role == 'developer':
|
||||
return True
|
||||
|
||||
# Check user role
|
||||
if hasattr(request.user, 'role'):
|
||||
role = request.user.role
|
||||
|
||||
@@ -22,9 +22,22 @@ class DebugScopedRateThrottle(ScopedRateThrottle):
|
||||
def allow_request(self, request, view):
|
||||
"""
|
||||
Check if request should be throttled.
|
||||
Only bypasses for DEBUG mode or public requests.
|
||||
Enforces per-account throttling for all authenticated users.
|
||||
Bypasses for: DEBUG mode, superusers, developers, system accounts, and public requests.
|
||||
Enforces per-account throttling for regular users.
|
||||
"""
|
||||
# Bypass for superusers and developers
|
||||
if request.user and hasattr(request.user, 'is_authenticated') and request.user.is_authenticated:
|
||||
if getattr(request.user, 'is_superuser', False):
|
||||
return True
|
||||
if hasattr(request.user, 'role') and request.user.role == 'developer':
|
||||
return True
|
||||
# Bypass for system account users
|
||||
try:
|
||||
if hasattr(request.user, 'is_system_account_user') and request.user.is_system_account_user():
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Check if throttling should be bypassed
|
||||
debug_bypass = getattr(settings, 'DEBUG', False)
|
||||
env_bypass = getattr(settings, 'IGNY8_DEBUG_THROTTLE', False)
|
||||
|
||||
@@ -31,14 +31,6 @@ class AccountContextMiddleware(MiddlewareMixin):
|
||||
# First, try to get user from Django session (cookie-based auth)
|
||||
# This handles cases where frontend uses credentials: 'include' with session cookies
|
||||
if hasattr(request, 'user') and request.user and request.user.is_authenticated:
|
||||
# Block superuser access via session on non-admin routes (JWT required)
|
||||
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
|
||||
if request.user.is_superuser and not auth_header.startswith('Bearer '):
|
||||
logout(request)
|
||||
return JsonResponse(
|
||||
{'success': False, 'error': 'Session authentication not allowed for API. Use JWT.'},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
# User is authenticated via session - refresh from DB to get latest account/plan data
|
||||
# This ensures changes to account/plan are reflected immediately without re-login
|
||||
try:
|
||||
@@ -141,7 +133,23 @@ class AccountContextMiddleware(MiddlewareMixin):
|
||||
"""
|
||||
Ensure the authenticated user has an account and an active plan.
|
||||
Uses shared validation helper for consistency.
|
||||
Bypasses validation for superusers, developers, and system accounts.
|
||||
"""
|
||||
# Bypass validation for superusers
|
||||
if getattr(user, 'is_superuser', False):
|
||||
return None
|
||||
|
||||
# Bypass validation for developers
|
||||
if hasattr(user, 'role') and user.role == 'developer':
|
||||
return None
|
||||
|
||||
# Bypass validation for system account users
|
||||
try:
|
||||
if hasattr(user, 'is_system_account_user') and user.is_system_account_user():
|
||||
return None
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
from .utils import validate_account_and_plan
|
||||
|
||||
is_valid, error_message, http_status = validate_account_and_plan(user)
|
||||
|
||||
@@ -135,6 +135,7 @@ def validate_account_and_plan(user_or_account):
|
||||
"""
|
||||
Validate account exists and has active plan.
|
||||
Allows trial, active, and pending_payment statuses.
|
||||
Bypasses validation for superusers, developers, and system accounts.
|
||||
|
||||
Args:
|
||||
user_or_account: User or Account instance
|
||||
@@ -145,6 +146,22 @@ def validate_account_and_plan(user_or_account):
|
||||
from rest_framework import status
|
||||
from .models import User, Account
|
||||
|
||||
# Bypass validation for superusers
|
||||
if isinstance(user_or_account, User):
|
||||
if getattr(user_or_account, 'is_superuser', False):
|
||||
return (True, None, None)
|
||||
|
||||
# Bypass validation for developers
|
||||
if hasattr(user_or_account, 'role') and user_or_account.role == 'developer':
|
||||
return (True, None, None)
|
||||
|
||||
# Bypass validation for system account users
|
||||
try:
|
||||
if hasattr(user_or_account, 'is_system_account_user') and user_or_account.is_system_account_user():
|
||||
return (True, None, None)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Extract account from user or use directly
|
||||
if isinstance(user_or_account, User):
|
||||
try:
|
||||
@@ -153,6 +170,12 @@ def validate_account_and_plan(user_or_account):
|
||||
account = None
|
||||
elif isinstance(user_or_account, Account):
|
||||
account = user_or_account
|
||||
# Check if account is a system account
|
||||
try:
|
||||
if hasattr(account, 'is_system_account') and account.is_system_account():
|
||||
return (True, None, None)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
return (False, 'Invalid object type', status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
"""Billing routes including bank transfer confirmation and credit endpoints."""
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import BillingViewSet
|
||||
from .views import (
|
||||
BillingViewSet,
|
||||
InvoiceViewSet,
|
||||
PaymentViewSet,
|
||||
CreditPackageViewSet,
|
||||
AccountPaymentMethodViewSet,
|
||||
)
|
||||
from igny8_core.modules.billing.views import (
|
||||
CreditBalanceViewSet,
|
||||
CreditUsageViewSet,
|
||||
@@ -14,6 +20,11 @@ router.register(r'admin', BillingViewSet, basename='billing-admin')
|
||||
router.register(r'credits/balance', CreditBalanceViewSet, basename='credit-balance')
|
||||
router.register(r'credits/usage', CreditUsageViewSet, basename='credit-usage')
|
||||
router.register(r'credits/transactions', CreditTransactionViewSet, basename='credit-transactions')
|
||||
# User-facing billing endpoints
|
||||
router.register(r'invoices', InvoiceViewSet, basename='invoices')
|
||||
router.register(r'payments', PaymentViewSet, basename='payments')
|
||||
router.register(r'credit-packages', CreditPackageViewSet, basename='credit-packages')
|
||||
router.register(r'payment-methods', AccountPaymentMethodViewSet, basename='payment-methods')
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
|
||||
@@ -3,14 +3,19 @@ Billing Views - Payment confirmation and management
|
||||
"""
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
from django.http import HttpResponse
|
||||
from datetime import timedelta
|
||||
from igny8_core.api.response import success_response, error_response
|
||||
from igny8_core.api.permissions import IsAdminOrOwner
|
||||
from igny8_core.api.response import success_response, error_response, paginated_response
|
||||
from igny8_core.api.permissions import IsAdminOrOwner, IsAuthenticatedAndActive, HasTenantAccess
|
||||
from igny8_core.api.base import AccountModelViewSet
|
||||
from igny8_core.api.pagination import CustomPageNumberPagination
|
||||
from igny8_core.auth.models import Account, Subscription
|
||||
from igny8_core.business.billing.services.credit_service import CreditService
|
||||
from igny8_core.business.billing.models import CreditTransaction
|
||||
from igny8_core.business.billing.services.invoice_service import InvoiceService
|
||||
from igny8_core.business.billing.models import CreditTransaction, Invoice, Payment, CreditPackage, AccountPaymentMethod
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -166,3 +171,242 @@ class BillingViewSet(viewsets.GenericViewSet):
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
|
||||
|
||||
class InvoiceViewSet(AccountModelViewSet):
|
||||
"""ViewSet for user-facing invoices"""
|
||||
queryset = Invoice.objects.all().select_related('account')
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
||||
pagination_class = CustomPageNumberPagination
|
||||
|
||||
def get_queryset(self):
|
||||
"""Filter invoices by account"""
|
||||
queryset = super().get_queryset()
|
||||
if hasattr(self.request, 'account') and self.request.account:
|
||||
queryset = queryset.filter(account=self.request.account)
|
||||
return queryset.order_by('-invoice_date', '-created_at')
|
||||
|
||||
def list(self, request):
|
||||
"""List invoices for current account"""
|
||||
queryset = self.get_queryset()
|
||||
|
||||
# Filter by status if provided
|
||||
status_param = request.query_params.get('status')
|
||||
if status_param:
|
||||
queryset = queryset.filter(status=status_param)
|
||||
|
||||
paginator = self.pagination_class()
|
||||
page = paginator.paginate_queryset(queryset, request)
|
||||
|
||||
# Serialize invoice data
|
||||
results = []
|
||||
for invoice in page:
|
||||
results.append({
|
||||
'id': invoice.id,
|
||||
'invoice_number': invoice.invoice_number,
|
||||
'status': invoice.status,
|
||||
'total_amount': str(invoice.total),
|
||||
'subtotal': str(invoice.subtotal),
|
||||
'tax_amount': str(invoice.tax),
|
||||
'currency': invoice.currency,
|
||||
'invoice_date': invoice.invoice_date.isoformat(),
|
||||
'due_date': invoice.due_date.isoformat(),
|
||||
'paid_at': invoice.paid_at.isoformat() if invoice.paid_at else None,
|
||||
'line_items': invoice.line_items,
|
||||
'billing_email': invoice.billing_email,
|
||||
'notes': invoice.notes,
|
||||
'created_at': invoice.created_at.isoformat(),
|
||||
})
|
||||
|
||||
paginated_data = paginator.get_paginated_response({'results': results}).data
|
||||
return paginated_response(paginated_data, request=request)
|
||||
|
||||
def retrieve(self, request, pk=None):
|
||||
"""Get invoice detail"""
|
||||
try:
|
||||
invoice = self.get_queryset().get(pk=pk)
|
||||
data = {
|
||||
'id': invoice.id,
|
||||
'invoice_number': invoice.invoice_number,
|
||||
'status': invoice.status,
|
||||
'total_amount': str(invoice.total),
|
||||
'subtotal': str(invoice.subtotal),
|
||||
'tax_amount': str(invoice.tax),
|
||||
'currency': invoice.currency,
|
||||
'invoice_date': invoice.invoice_date.isoformat(),
|
||||
'due_date': invoice.due_date.isoformat(),
|
||||
'paid_at': invoice.paid_at.isoformat() if invoice.paid_at else None,
|
||||
'line_items': invoice.line_items,
|
||||
'billing_email': invoice.billing_email,
|
||||
'notes': invoice.notes,
|
||||
'created_at': invoice.created_at.isoformat(),
|
||||
}
|
||||
return success_response(data=data, request=request)
|
||||
except Invoice.DoesNotExist:
|
||||
return error_response(error='Invoice not found', status_code=404, request=request)
|
||||
|
||||
@action(detail=True, methods=['get'])
|
||||
def download_pdf(self, request, pk=None):
|
||||
"""Download invoice PDF"""
|
||||
try:
|
||||
invoice = self.get_queryset().get(pk=pk)
|
||||
pdf_bytes = InvoiceService.generate_pdf(invoice)
|
||||
|
||||
response = HttpResponse(pdf_bytes, content_type='application/pdf')
|
||||
response['Content-Disposition'] = f'attachment; filename="invoice-{invoice.invoice_number}.pdf"'
|
||||
return response
|
||||
except Invoice.DoesNotExist:
|
||||
return error_response(error='Invoice not found', status_code=404, request=request)
|
||||
|
||||
|
||||
class PaymentViewSet(AccountModelViewSet):
|
||||
"""ViewSet for user-facing payments"""
|
||||
queryset = Payment.objects.all().select_related('account', 'invoice')
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
||||
pagination_class = CustomPageNumberPagination
|
||||
|
||||
def get_queryset(self):
|
||||
"""Filter payments by account"""
|
||||
queryset = super().get_queryset()
|
||||
if hasattr(self.request, 'account') and self.request.account:
|
||||
queryset = queryset.filter(account=self.request.account)
|
||||
return queryset.order_by('-created_at')
|
||||
|
||||
def list(self, request):
|
||||
"""List payments for current account"""
|
||||
queryset = self.get_queryset()
|
||||
|
||||
# Filter by status if provided
|
||||
status_param = request.query_params.get('status')
|
||||
if status_param:
|
||||
queryset = queryset.filter(status=status_param)
|
||||
|
||||
# Filter by invoice if provided
|
||||
invoice_id = request.query_params.get('invoice_id')
|
||||
if invoice_id:
|
||||
queryset = queryset.filter(invoice_id=invoice_id)
|
||||
|
||||
paginator = self.pagination_class()
|
||||
page = paginator.paginate_queryset(queryset, request)
|
||||
|
||||
# Serialize payment data
|
||||
results = []
|
||||
for payment in page:
|
||||
results.append({
|
||||
'id': payment.id,
|
||||
'invoice_id': payment.invoice_id,
|
||||
'invoice_number': payment.invoice.invoice_number if payment.invoice else None,
|
||||
'amount': str(payment.amount),
|
||||
'currency': payment.currency,
|
||||
'status': payment.status,
|
||||
'payment_method': payment.payment_method,
|
||||
'created_at': payment.created_at.isoformat(),
|
||||
'processed_at': payment.processed_at.isoformat() if payment.processed_at else None,
|
||||
'manual_reference': payment.manual_reference,
|
||||
'manual_notes': payment.manual_notes,
|
||||
})
|
||||
|
||||
paginated_data = paginator.get_paginated_response({'results': results}).data
|
||||
return paginated_response(paginated_data, request=request)
|
||||
|
||||
@action(detail=False, methods=['post'])
|
||||
def manual(self, request):
|
||||
"""Submit manual payment for approval"""
|
||||
invoice_id = request.data.get('invoice_id')
|
||||
amount = request.data.get('amount')
|
||||
payment_method = request.data.get('payment_method', 'bank_transfer')
|
||||
reference = request.data.get('reference', '')
|
||||
notes = request.data.get('notes', '')
|
||||
|
||||
if not amount:
|
||||
return error_response(error='Amount is required', status_code=400, request=request)
|
||||
|
||||
try:
|
||||
account = request.account
|
||||
invoice = None
|
||||
if invoice_id:
|
||||
invoice = Invoice.objects.get(id=invoice_id, account=account)
|
||||
|
||||
payment = Payment.objects.create(
|
||||
account=account,
|
||||
invoice=invoice,
|
||||
amount=amount,
|
||||
currency='USD',
|
||||
payment_method=payment_method,
|
||||
status='pending_approval',
|
||||
manual_reference=reference,
|
||||
manual_notes=notes,
|
||||
)
|
||||
|
||||
return success_response(
|
||||
data={'id': payment.id, 'status': payment.status},
|
||||
message='Manual payment submitted for approval',
|
||||
status_code=201,
|
||||
request=request
|
||||
)
|
||||
except Invoice.DoesNotExist:
|
||||
return error_response(error='Invoice not found', status_code=404, request=request)
|
||||
|
||||
|
||||
class CreditPackageViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""ViewSet for credit packages (read-only for users)"""
|
||||
queryset = CreditPackage.objects.filter(is_active=True).order_by('sort_order')
|
||||
permission_classes = [IsAuthenticatedAndActive]
|
||||
pagination_class = CustomPageNumberPagination
|
||||
|
||||
def list(self, request):
|
||||
"""List available credit packages"""
|
||||
queryset = self.get_queryset()
|
||||
paginator = self.pagination_class()
|
||||
page = paginator.paginate_queryset(queryset, request)
|
||||
|
||||
results = []
|
||||
for package in page:
|
||||
results.append({
|
||||
'id': package.id,
|
||||
'name': package.name,
|
||||
'slug': package.slug,
|
||||
'credits': package.credits,
|
||||
'price': str(package.price),
|
||||
'discount_percentage': package.discount_percentage,
|
||||
'is_featured': package.is_featured,
|
||||
'description': package.description,
|
||||
'display_order': package.sort_order,
|
||||
})
|
||||
|
||||
paginated_data = paginator.get_paginated_response({'results': results}).data
|
||||
return paginated_response(paginated_data, request=request)
|
||||
|
||||
|
||||
class AccountPaymentMethodViewSet(AccountModelViewSet):
|
||||
"""ViewSet for account payment methods"""
|
||||
queryset = AccountPaymentMethod.objects.all()
|
||||
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
|
||||
pagination_class = CustomPageNumberPagination
|
||||
|
||||
def get_queryset(self):
|
||||
"""Filter payment methods by account"""
|
||||
queryset = super().get_queryset()
|
||||
if hasattr(self.request, 'account') and self.request.account:
|
||||
queryset = queryset.filter(account=self.request.account)
|
||||
return queryset.order_by('-is_default', 'type')
|
||||
|
||||
def list(self, request):
|
||||
"""List payment methods for current account"""
|
||||
queryset = self.get_queryset()
|
||||
paginator = self.pagination_class()
|
||||
page = paginator.paginate_queryset(queryset, request)
|
||||
|
||||
results = []
|
||||
for method in page:
|
||||
results.append({
|
||||
'id': str(method.id),
|
||||
'type': method.type,
|
||||
'display_name': method.display_name,
|
||||
'is_default': method.is_default,
|
||||
'is_enabled': method.is_enabled if hasattr(method, 'is_enabled') else True,
|
||||
'instructions': method.instructions,
|
||||
})
|
||||
|
||||
paginated_data = paginator.get_paginated_response({'results': results}).data
|
||||
return paginated_response(paginated_data, request=request)
|
||||
|
||||
@@ -22,6 +22,8 @@ urlpatterns = [
|
||||
path('account_balance/', BillingOverviewViewSet.as_view({'get': 'account_balance'}), name='account-balance'),
|
||||
# Canonical credit balance endpoint
|
||||
path('credits/balance/', CreditBalanceViewSet.as_view({'get': 'list'}), name='credit-balance-canonical'),
|
||||
# Alias for frontend compatibility (transactions/balance/)
|
||||
path('transactions/balance/', CreditBalanceViewSet.as_view({'get': 'list'}), name='transactions-balance'),
|
||||
# Explicit list endpoints
|
||||
path('transactions/', CreditTransactionViewSet.as_view({'get': 'list'}), name='transactions'),
|
||||
path('usage/', CreditUsageViewSet.as_view({'get': 'list'}), name='usage'),
|
||||
|
||||
Reference in New Issue
Block a user