Enhance API structure and documentation: Added new tags for Account, Integration, Automation, Linker, Optimizer, and Publisher; updated billing endpoints for admin and customer; improved API reference documentation; fixed endpoint paths in frontend services.

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-07 01:13:38 +00:00
parent dc9dba2c9e
commit 7a2b424237
15 changed files with 314 additions and 24 deletions

View File

@@ -10,6 +10,7 @@ from django.contrib.auth import get_user_model
from django.db.models import Q, Count, Sum from django.db.models import Q, Count, Sum
from django.utils import timezone from django.utils import timezone
from datetime import timedelta from datetime import timedelta
from drf_spectacular.utils import extend_schema, extend_schema_view
from igny8_core.auth.models import Account from igny8_core.auth.models import Account
from igny8_core.business.billing.models import CreditTransaction from igny8_core.business.billing.models import CreditTransaction
@@ -17,6 +18,10 @@ from igny8_core.business.billing.models import CreditTransaction
User = get_user_model() User = get_user_model()
@extend_schema_view(
retrieve=extend_schema(tags=['Account']),
partial_update=extend_schema(tags=['Account']),
)
class AccountSettingsViewSet(viewsets.ViewSet): class AccountSettingsViewSet(viewsets.ViewSet):
"""Account settings management""" """Account settings management"""
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
@@ -77,6 +82,11 @@ class AccountSettingsViewSet(viewsets.ViewSet):
}) })
@extend_schema_view(
list=extend_schema(tags=['Account']),
create=extend_schema(tags=['Account']),
destroy=extend_schema(tags=['Account']),
)
class TeamManagementViewSet(viewsets.ViewSet): class TeamManagementViewSet(viewsets.ViewSet):
"""Team members management""" """Team members management"""
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
@@ -166,6 +176,9 @@ class TeamManagementViewSet(viewsets.ViewSet):
) )
@extend_schema_view(
overview=extend_schema(tags=['Account']),
)
class UsageAnalyticsViewSet(viewsets.ViewSet): class UsageAnalyticsViewSet(viewsets.ViewSet):
"""Usage analytics and statistics""" """Usage analytics and statistics"""
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]

View File

@@ -8,7 +8,20 @@ from drf_spectacular.utils import extend_schema, OpenApiResponse
from rest_framework import status from rest_framework import status
# Explicit tags we want to keep (from SPECTACULAR_SETTINGS) # Explicit tags we want to keep (from SPECTACULAR_SETTINGS)
EXPLICIT_TAGS = {'Authentication', 'Planner', 'Writer', 'System', 'Billing'} EXPLICIT_TAGS = {
'Authentication',
'Planner',
'Writer',
'System',
'Billing',
'Account',
'Automation',
'Linker',
'Optimizer',
'Publisher',
'Integration',
'Admin Billing',
}
def postprocess_schema_filter_tags(result, generator, request, public): def postprocess_schema_filter_tags(result, generator, request, public):
@@ -41,6 +54,20 @@ def postprocess_schema_filter_tags(result, generator, request, public):
filtered_tags = ['System'] filtered_tags = ['System']
elif '/billing/' in path or '/api/v1/billing/' in path: elif '/billing/' in path or '/api/v1/billing/' in path:
filtered_tags = ['Billing'] filtered_tags = ['Billing']
elif '/account/' in path or '/api/v1/account/' in path:
filtered_tags = ['Account']
elif '/automation/' in path or '/api/v1/automation/' in path:
filtered_tags = ['Automation']
elif '/linker/' in path or '/api/v1/linker/' in path:
filtered_tags = ['Linker']
elif '/optimizer/' in path or '/api/v1/optimizer/' in path:
filtered_tags = ['Optimizer']
elif '/publisher/' in path or '/api/v1/publisher/' in path:
filtered_tags = ['Publisher']
elif '/integration/' in path or '/api/v1/integration/' in path:
filtered_tags = ['Integration']
elif '/admin/' in path or '/api/v1/admin/' in path:
filtered_tags = ['Admin Billing']
operation['tags'] = filtered_tags operation['tags'] = filtered_tags

View File

@@ -8,6 +8,7 @@ from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils import timezone from django.utils import timezone
from drf_spectacular.utils import extend_schema
from igny8_core.business.automation.models import AutomationConfig, AutomationRun from igny8_core.business.automation.models import AutomationConfig, AutomationRun
from igny8_core.business.automation.services import AutomationService from igny8_core.business.automation.services import AutomationService
@@ -30,6 +31,7 @@ class AutomationViewSet(viewsets.ViewSet):
site = get_object_or_404(Site, id=site_id, account=request.user.account) site = get_object_or_404(Site, id=site_id, account=request.user.account)
return site, None return site, None
@extend_schema(tags=['Automation'])
@action(detail=False, methods=['get']) @action(detail=False, methods=['get'])
def config(self, request): def config(self, request):
""" """
@@ -68,6 +70,7 @@ class AutomationViewSet(viewsets.ViewSet):
'next_run_at': config.next_run_at, 'next_run_at': config.next_run_at,
}) })
@extend_schema(tags=['Automation'])
@action(detail=False, methods=['put']) @action(detail=False, methods=['put'])
def update_config(self, request): def update_config(self, request):
""" """
@@ -142,6 +145,7 @@ class AutomationViewSet(viewsets.ViewSet):
'next_run_at': config.next_run_at, 'next_run_at': config.next_run_at,
}) })
@extend_schema(tags=['Automation'])
@action(detail=False, methods=['post']) @action(detail=False, methods=['post'])
def run_now(self, request): def run_now(self, request):
""" """
@@ -175,6 +179,7 @@ class AutomationViewSet(viewsets.ViewSet):
status=status.HTTP_500_INTERNAL_SERVER_ERROR status=status.HTTP_500_INTERNAL_SERVER_ERROR
) )
@extend_schema(tags=['Automation'])
@action(detail=False, methods=['get']) @action(detail=False, methods=['get'])
def current_run(self, request): def current_run(self, request):
""" """
@@ -211,6 +216,7 @@ class AutomationViewSet(viewsets.ViewSet):
} }
}) })
@extend_schema(tags=['Automation'])
@action(detail=False, methods=['post']) @action(detail=False, methods=['post'])
def pause(self, request): def pause(self, request):
""" """
@@ -234,6 +240,7 @@ class AutomationViewSet(viewsets.ViewSet):
status=status.HTTP_404_NOT_FOUND status=status.HTTP_404_NOT_FOUND
) )
@extend_schema(tags=['Automation'])
@action(detail=False, methods=['post']) @action(detail=False, methods=['post'])
def resume(self, request): def resume(self, request):
""" """
@@ -262,6 +269,7 @@ class AutomationViewSet(viewsets.ViewSet):
status=status.HTTP_404_NOT_FOUND status=status.HTTP_404_NOT_FOUND
) )
@extend_schema(tags=['Automation'])
@action(detail=False, methods=['get']) @action(detail=False, methods=['get'])
def history(self, request): def history(self, request):
""" """
@@ -291,6 +299,7 @@ class AutomationViewSet(viewsets.ViewSet):
] ]
}) })
@extend_schema(tags=['Automation'])
@action(detail=False, methods=['get']) @action(detail=False, methods=['get'])
def logs(self, request): def logs(self, request):
""" """
@@ -323,6 +332,7 @@ class AutomationViewSet(viewsets.ViewSet):
status=status.HTTP_404_NOT_FOUND status=status.HTTP_404_NOT_FOUND
) )
@extend_schema(tags=['Automation'])
@action(detail=False, methods=['get']) @action(detail=False, methods=['get'])
def estimate(self, request): def estimate(self, request):
""" """
@@ -342,6 +352,7 @@ class AutomationViewSet(viewsets.ViewSet):
'sufficient': site.account.credits >= (estimated_credits * 1.2) 'sufficient': site.account.credits >= (estimated_credits * 1.2)
}) })
@extend_schema(tags=['Automation'])
@action(detail=False, methods=['get']) @action(detail=False, methods=['get'])
def pipeline_overview(self, request): def pipeline_overview(self, request):
""" """
@@ -504,6 +515,7 @@ class AutomationViewSet(viewsets.ViewSet):
] ]
}) })
@extend_schema(tags=['Automation'])
@action(detail=False, methods=['get'], url_path='current_processing') @action(detail=False, methods=['get'], url_path='current_processing')
def current_processing(self, request): def current_processing(self, request):
""" """
@@ -547,6 +559,7 @@ class AutomationViewSet(viewsets.ViewSet):
status=status.HTTP_500_INTERNAL_SERVER_ERROR status=status.HTTP_500_INTERNAL_SERVER_ERROR
) )
@extend_schema(tags=['Automation'])
@action(detail=False, methods=['post'], url_path='pause') @action(detail=False, methods=['post'], url_path='pause')
def pause_automation(self, request): def pause_automation(self, request):
""" """
@@ -596,6 +609,7 @@ class AutomationViewSet(viewsets.ViewSet):
status=status.HTTP_500_INTERNAL_SERVER_ERROR status=status.HTTP_500_INTERNAL_SERVER_ERROR
) )
@extend_schema(tags=['Automation'])
@action(detail=False, methods=['post'], url_path='resume') @action(detail=False, methods=['post'], url_path='resume')
def resume_automation(self, request): def resume_automation(self, request):
""" """
@@ -649,6 +663,7 @@ class AutomationViewSet(viewsets.ViewSet):
status=status.HTTP_500_INTERNAL_SERVER_ERROR status=status.HTTP_500_INTERNAL_SERVER_ERROR
) )
@extend_schema(tags=['Automation'])
@action(detail=False, methods=['post'], url_path='cancel') @action(detail=False, methods=['post'], url_path='cancel')
def cancel_automation(self, request): def cancel_automation(self, request):
""" """

View File

@@ -11,14 +11,21 @@ from .views import (
AdminBillingViewSet, AdminBillingViewSet,
AccountPaymentMethodViewSet, AccountPaymentMethodViewSet,
) )
from igny8_core.modules.billing.views import (
CreditBalanceViewSet,
CreditUsageViewSet,
)
router = DefaultRouter() router = DefaultRouter()
router.register(r'invoices', InvoiceViewSet, basename='invoice') router.register(r'invoices', InvoiceViewSet, basename='invoice')
router.register(r'payments', PaymentViewSet, basename='payment') router.register(r'payments', PaymentViewSet, basename='payment')
router.register(r'credit-packages', CreditPackageViewSet, basename='credit-package') router.register(r'credit-packages', CreditPackageViewSet, basename='credit-package')
router.register(r'transactions', CreditTransactionViewSet, basename='transaction') router.register(r'transactions', CreditTransactionViewSet, basename='transaction')
router.register(r'admin', AdminBillingViewSet, basename='admin-billing')
router.register(r'payment-methods', AccountPaymentMethodViewSet, basename='payment-method') router.register(r'payment-methods', AccountPaymentMethodViewSet, basename='payment-method')
# Canonical credits endpoints (unified billing)
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')
urlpatterns = [ urlpatterns = [
# Country/config-driven available methods (legacy alias) # Country/config-driven available methods (legacy alias)

View File

@@ -9,6 +9,7 @@ from rest_framework.permissions import IsAuthenticated
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.db import models from django.db import models
from drf_spectacular.utils import extend_schema, extend_schema_view
from .models import ( from .models import (
Invoice, Invoice,
@@ -366,6 +367,14 @@ class CreditTransactionViewSet(viewsets.ViewSet):
}) })
@extend_schema_view(
invoices=extend_schema(tags=['Admin Billing']),
payments=extend_schema(tags=['Admin Billing']),
pending_payments=extend_schema(tags=['Admin Billing']),
approve_payment=extend_schema(tags=['Admin Billing']),
reject_payment=extend_schema(tags=['Admin Billing']),
stats=extend_schema(tags=['Admin Billing']),
)
class AdminBillingViewSet(viewsets.ViewSet): class AdminBillingViewSet(viewsets.ViewSet):
"""Admin billing management""" """Admin billing management"""
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]

View File

@@ -1,6 +1,10 @@
from django.urls import path from django.urls import path
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from .views import AdminBillingViewSet from .views import AdminBillingViewSet
from igny8_core.business.billing.views import (
AdminBillingViewSet as BillingAdminViewSet,
)
router = DefaultRouter() router = DefaultRouter()
@@ -9,6 +13,12 @@ urlpatterns = [
path('users/', AdminBillingViewSet.as_view({'get': 'list_users'}), name='admin-users-list'), path('users/', AdminBillingViewSet.as_view({'get': 'list_users'}), name='admin-users-list'),
path('users/<int:user_id>/adjust-credits/', AdminBillingViewSet.as_view({'post': 'adjust_credits'}), name='admin-adjust-credits'), path('users/<int:user_id>/adjust-credits/', AdminBillingViewSet.as_view({'post': 'adjust_credits'}), name='admin-adjust-credits'),
path('credit-costs/', AdminBillingViewSet.as_view({'get': 'list_credit_costs', 'post': 'update_credit_costs'}), name='admin-credit-costs'), path('credit-costs/', AdminBillingViewSet.as_view({'get': 'list_credit_costs', 'post': 'update_credit_costs'}), name='admin-credit-costs'),
# Unified admin billing endpoints (alias legacy /billing/admin/* under /admin/billing/*)
path('billing/invoices/', BillingAdminViewSet.as_view({'get': 'invoices'}), name='admin-billing-invoices'),
path('billing/payments/', BillingAdminViewSet.as_view({'get': 'payments'}), name='admin-billing-payments'),
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'),
] ]
urlpatterns += router.urls urlpatterns += router.urls

View File

@@ -20,6 +20,9 @@ urlpatterns = [
path('', include(router.urls)), path('', include(router.urls)),
# User-facing billing overview # User-facing billing overview
path('account_balance/', BillingOverviewViewSet.as_view({'get': 'account_balance'}), name='account-balance'), 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'),
# Explicit list endpoints
path('transactions/', CreditTransactionViewSet.as_view({'get': 'list'}), name='transactions'), path('transactions/', CreditTransactionViewSet.as_view({'get': 'list'}), name='transactions'),
path('usage/', CreditUsageViewSet.as_view({'get': 'list'}), name='usage'), path('usage/', CreditUsageViewSet.as_view({'get': 'list'}), name='usage'),
# Admin billing endpoints # Admin billing endpoints

View File

@@ -26,7 +26,7 @@ from .exceptions import InsufficientCreditsError
@extend_schema_view( @extend_schema_view(
list=extend_schema(tags=['Billing']), list=extend_schema(tags=['Billing'], summary='Get credit balance'),
) )
class CreditBalanceViewSet(viewsets.ViewSet): class CreditBalanceViewSet(viewsets.ViewSet):
""" """
@@ -38,8 +38,7 @@ class CreditBalanceViewSet(viewsets.ViewSet):
throttle_scope = 'billing' throttle_scope = 'billing'
throttle_classes = [DebugScopedRateThrottle] throttle_classes = [DebugScopedRateThrottle]
@action(detail=False, methods=['get']) def list(self, request):
def balance(self, request):
"""Get current credit balance and usage""" """Get current credit balance and usage"""
account = getattr(request, 'account', None) account = getattr(request, 'account', None)
if not account: if not account:
@@ -125,6 +124,7 @@ class CreditUsageViewSet(AccountModelViewSet):
return queryset.order_by('-created_at') return queryset.order_by('-created_at')
@extend_schema(tags=['Billing'], summary='Get usage summary')
@action(detail=False, methods=['get']) @action(detail=False, methods=['get'])
def summary(self, request): def summary(self, request):
"""Get usage summary for date range""" """Get usage summary for date range"""
@@ -214,6 +214,7 @@ class CreditUsageViewSet(AccountModelViewSet):
serializer = UsageSummarySerializer(data) serializer = UsageSummarySerializer(data)
return success_response(data=serializer.data, request=request) return success_response(data=serializer.data, request=request)
@extend_schema(tags=['Billing'], summary='Get usage limits')
@action(detail=False, methods=['get'], url_path='limits', url_name='limits') @action(detail=False, methods=['get'], url_path='limits', url_name='limits')
def limits(self, request): def limits(self, request):
""" """
@@ -434,6 +435,13 @@ class BillingOverviewViewSet(viewsets.ViewSet):
return Response(data) return Response(data)
@extend_schema_view(
stats=extend_schema(tags=['Admin Billing'], summary='Admin billing stats'),
list_users=extend_schema(tags=['Admin Billing'], summary='List users with credit info'),
adjust_credits=extend_schema(tags=['Admin Billing'], summary='Adjust user credits'),
list_credit_costs=extend_schema(tags=['Admin Billing'], summary='List credit cost configurations'),
update_credit_costs=extend_schema(tags=['Admin Billing'], summary='Update credit cost configurations'),
)
class AdminBillingViewSet(viewsets.ViewSet): class AdminBillingViewSet(viewsets.ViewSet):
"""Admin-only billing management API""" """Admin-only billing management API"""
permission_classes = [IsAuthenticatedAndActive, permissions.IsAdminUser] permission_classes = [IsAuthenticatedAndActive, permissions.IsAdminUser]

View File

@@ -6,6 +6,7 @@ from rest_framework import status
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from django.utils import timezone from django.utils import timezone
from drf_spectacular.utils import extend_schema, extend_schema_view
from igny8_core.api.base import SiteSectorModelViewSet from igny8_core.api.base import SiteSectorModelViewSet
from igny8_core.api.permissions import IsAuthenticatedAndActive, IsEditorOrAbove from igny8_core.api.permissions import IsAuthenticatedAndActive, IsEditorOrAbove
@@ -21,6 +22,14 @@ import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@extend_schema_view(
list=extend_schema(tags=['Integration']),
create=extend_schema(tags=['Integration']),
retrieve=extend_schema(tags=['Integration']),
update=extend_schema(tags=['Integration']),
partial_update=extend_schema(tags=['Integration']),
destroy=extend_schema(tags=['Integration']),
)
class IntegrationViewSet(SiteSectorModelViewSet): class IntegrationViewSet(SiteSectorModelViewSet):
""" """
ViewSet for SiteIntegration model. ViewSet for SiteIntegration model.
@@ -88,6 +97,7 @@ class IntegrationViewSet(SiteSectorModelViewSet):
return SiteIntegrationSerializer return SiteIntegrationSerializer
@extend_schema(tags=['Integration'])
@action(detail=True, methods=['post']) @action(detail=True, methods=['post'])
def test_connection(self, request, pk=None): def test_connection(self, request, pk=None):
""" """
@@ -118,6 +128,7 @@ class IntegrationViewSet(SiteSectorModelViewSet):
def allow_request(self, request, view): def allow_request(self, request, view):
return True return True
@extend_schema(tags=['Integration'])
@action(detail=False, methods=['post'], url_path='test-connection', @action(detail=False, methods=['post'], url_path='test-connection',
permission_classes=[AllowAny], throttle_classes=[NoThrottle]) permission_classes=[AllowAny], throttle_classes=[NoThrottle])
def test_connection_collection(self, request): def test_connection_collection(self, request):
@@ -221,6 +232,7 @@ class IntegrationViewSet(SiteSectorModelViewSet):
logger.info(f"[IntegrationViewSet] Deleted integration {integration.id} due to failed connection test") logger.info(f"[IntegrationViewSet] Deleted integration {integration.id} due to failed connection test")
return error_response(result.get('message', 'Connection test failed'), None, status.HTTP_400_BAD_REQUEST, request) return error_response(result.get('message', 'Connection test failed'), None, status.HTTP_400_BAD_REQUEST, request)
@extend_schema(tags=['Integration'])
@action(detail=True, methods=['post']) @action(detail=True, methods=['post'])
def sync(self, request, pk=None): def sync(self, request, pk=None):
""" """
@@ -252,6 +264,7 @@ class IntegrationViewSet(SiteSectorModelViewSet):
response_status = status.HTTP_200_OK if result.get('success') else status.HTTP_400_BAD_REQUEST response_status = status.HTTP_200_OK if result.get('success') else status.HTTP_400_BAD_REQUEST
return success_response(result, request=request, status_code=response_status) return success_response(result, request=request, status_code=response_status)
@extend_schema(tags=['Integration'])
@action(detail=True, methods=['get']) @action(detail=True, methods=['get'])
def sync_status(self, request, pk=None): def sync_status(self, request, pk=None):
""" """

View File

@@ -11,6 +11,7 @@ from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from drf_spectacular.utils import extend_schema, extend_schema_view
from igny8_core.api.base import SiteSectorModelViewSet from igny8_core.api.base import SiteSectorModelViewSet
from igny8_core.api.permissions import IsAuthenticatedAndActive, IsEditorOrAbove from igny8_core.api.permissions import IsAuthenticatedAndActive, IsEditorOrAbove
@@ -20,6 +21,14 @@ from igny8_core.business.publishing.models import PublishingRecord, DeploymentRe
from igny8_core.business.publishing.services.publisher_service import PublisherService from igny8_core.business.publishing.services.publisher_service import PublisherService
@extend_schema_view(
list=extend_schema(tags=['Publisher']),
create=extend_schema(tags=['Publisher']),
retrieve=extend_schema(tags=['Publisher']),
update=extend_schema(tags=['Publisher']),
partial_update=extend_schema(tags=['Publisher']),
destroy=extend_schema(tags=['Publisher']),
)
class PublishingRecordViewSet(SiteSectorModelViewSet): class PublishingRecordViewSet(SiteSectorModelViewSet):
""" """
ViewSet for PublishingRecord model. ViewSet for PublishingRecord model.
@@ -41,6 +50,14 @@ class PublishingRecordViewSet(SiteSectorModelViewSet):
return PublishingRecordSerializer return PublishingRecordSerializer
@extend_schema_view(
list=extend_schema(tags=['Publisher']),
create=extend_schema(tags=['Publisher']),
retrieve=extend_schema(tags=['Publisher']),
update=extend_schema(tags=['Publisher']),
partial_update=extend_schema(tags=['Publisher']),
destroy=extend_schema(tags=['Publisher']),
)
class DeploymentRecordViewSet(SiteSectorModelViewSet): class DeploymentRecordViewSet(SiteSectorModelViewSet):
""" """
ViewSet for DeploymentRecord model. ViewSet for DeploymentRecord model.
@@ -63,6 +80,7 @@ class DeploymentRecordViewSet(SiteSectorModelViewSet):
return DeploymentRecordSerializer return DeploymentRecordSerializer
@extend_schema_view()
class PublisherViewSet(viewsets.ViewSet): class PublisherViewSet(viewsets.ViewSet):
""" """
Publisher actions for publishing content. Publisher actions for publishing content.
@@ -76,6 +94,7 @@ class PublisherViewSet(viewsets.ViewSet):
super().__init__(**kwargs) super().__init__(**kwargs)
self.publisher_service = PublisherService() self.publisher_service = PublisherService()
@extend_schema(tags=['Publisher'])
@action(detail=False, methods=['post'], url_path='publish') @action(detail=False, methods=['post'], url_path='publish')
def publish(self, request): def publish(self, request):
""" """

View File

@@ -1580,12 +1580,12 @@ class ContentViewSet(SiteSectorModelViewSet):
@extend_schema_view( @extend_schema_view(
list=extend_schema(tags=['Writer - Taxonomies']), list=extend_schema(tags=['Writer']),
create=extend_schema(tags=['Writer - Taxonomies']), create=extend_schema(tags=['Writer']),
retrieve=extend_schema(tags=['Writer - Taxonomies']), retrieve=extend_schema(tags=['Writer']),
update=extend_schema(tags=['Writer - Taxonomies']), update=extend_schema(tags=['Writer']),
partial_update=extend_schema(tags=['Writer - Taxonomies']), partial_update=extend_schema(tags=['Writer']),
destroy=extend_schema(tags=['Writer - Taxonomies']), destroy=extend_schema(tags=['Writer']),
) )
class ContentTaxonomyViewSet(SiteSectorModelViewSet): class ContentTaxonomyViewSet(SiteSectorModelViewSet):
""" """

View File

@@ -352,12 +352,32 @@ SPECTACULAR_SETTINGS = {
# Tag configuration - prevent auto-generation and use explicit tags # Tag configuration - prevent auto-generation and use explicit tags
'TAGS': [ 'TAGS': [
{'name': 'Authentication', 'description': 'User authentication and registration'}, {'name': 'Authentication', 'description': 'User authentication and registration'},
{'name': 'Account', 'description': 'Account settings, team, and usage analytics'},
{'name': 'Integration', 'description': 'Site integrations and sync'},
{'name': 'System', 'description': 'Settings, prompts, and integrations'},
{'name': 'Admin Billing', 'description': 'Admin-only billing management'},
{'name': 'Billing', 'description': 'Credits, usage, and transactions'},
{'name': 'Planner', 'description': 'Keywords, clusters, and content ideas'}, {'name': 'Planner', 'description': 'Keywords, clusters, and content ideas'},
{'name': 'Writer', 'description': 'Tasks, content, and images'}, {'name': 'Writer', 'description': 'Tasks, content, and images'},
{'name': 'System', 'description': 'Settings, prompts, and integrations'}, {'name': 'Automation', 'description': 'Automation configuration and runs'},
{'name': 'Billing', 'description': 'Credits, usage, and transactions'}, {'name': 'Linker', 'description': 'Internal linking operations'},
{'name': 'Optimizer', 'description': 'Content optimization operations'},
{'name': 'Publisher', 'description': 'Publishing records and deployments'},
],
'TAGS_ORDER': [
'Authentication',
'Account',
'Integration',
'System',
'Admin Billing',
'Billing',
'Planner',
'Writer',
'Automation',
'Linker',
'Optimizer',
'Publisher',
], ],
'TAGS_ORDER': ['Authentication', 'Planner', 'Writer', 'System', 'Billing'],
# Postprocessing hook to filter out auto-generated tags # Postprocessing hook to filter out auto-generated tags
'POSTPROCESSING_HOOKS': ['igny8_core.api.schema_extensions.postprocess_schema_filter_tags'], 'POSTPROCESSING_HOOKS': ['igny8_core.api.schema_extensions.postprocess_schema_filter_tags'],

View File

@@ -95,12 +95,35 @@ Development: http://localhost:8000/api/v1/
``` ```
/api/v1/ /api/v1/
├── auth/ # Authentication and user management ├── auth/ # Authentication and user management
├── account/ # Account settings, team, and usage analytics
├── integration/ # Site integrations and sync
├── system/ # Settings, prompts, integrations
├── admin/billing/ # Admin-only billing management
├── billing/ # Credits, transactions, usage
├── planner/ # Keywords, clusters, content ideas ├── planner/ # Keywords, clusters, content ideas
├── writer/ # Tasks, content, images ├── writer/ # Tasks, content, images
├── system/ # Settings, prompts, integrations ├── automation/ # Automation configuration and runs
── billing/ # Credits, transactions, usage ── linker/ # Internal linking operations
├── optimizer/ # Content optimization operations
└── publisher/ # Publishing records and deployments
``` ```
### Module → Tag Map (Swagger/ReDoc)
- Authentication → `Authentication`
- Account → `Account`
- Integration → `Integration`
- System → `System`
- Admin Billing → `Admin Billing`
- Billing → `Billing`
- Planner → `Planner`
- Writer → `Writer`
- Automation → `Automation`
- Linker → `Linker`
- Optimizer → `Optimizer`
- Publisher → `Publisher`
Tag display order (docs): Authentication, Account, Integration, System, Admin Billing, Billing, Planner, Writer, Automation, Linker, Optimizer, Publisher
### Technology Stack ### Technology Stack
- **Framework**: Django REST Framework (DRF) - **Framework**: Django REST Framework (DRF)
@@ -985,6 +1008,30 @@ class KeywordViewSet(SiteSectorModelViewSet):
- `GET /api/v1/system/ping/` - Health check endpoint (AllowAny) - `GET /api/v1/system/ping/` - Health check endpoint (AllowAny)
- `GET /api/v1/system/request-metrics/{request_id}/` - Get request metrics for debugging - `GET /api/v1/system/request-metrics/{request_id}/` - Get request metrics for debugging
### Admin Billing & Credits (Admin-only, Unified)
**Base Path**: `/api/v1/admin/billing/` (all admin billing/credits live here)
- `GET /api/v1/admin/billing/stats/` - System billing stats (admin-only)
- `GET /api/v1/admin/billing/invoices/` - Admin invoice listing (all accounts)
- `GET /api/v1/admin/billing/payments/` - Admin payment listing (all accounts)
- `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/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)
- `POST /api/v1/admin/users/{user_id}/adjust-credits/` - Adjust user credits (admin-only)
> Non-standard/legacy endpoints to deprecate and remove (do not use):
> - `/api/v1/billing/admin/stats/`
> - `/api/v1/billing/admin/invoices/`
> - `/api/v1/billing/admin/payments/`
> - `/api/v1/billing/admin/pending_payments/`
> - `/api/v1/billing/admin/{id}/approve_payment/`
> - `/api/v1/billing/admin/{id}/reject_payment/`
### Billing Module Endpoints ### Billing Module Endpoints
**Base Path**: `/api/v1/billing/` **Base Path**: `/api/v1/billing/`
@@ -994,7 +1041,7 @@ class KeywordViewSet(SiteSectorModelViewSet):
**Base Path**: `/api/v1/billing/credits/balance/` **Base Path**: `/api/v1/billing/credits/balance/`
**Permission**: IsAuthenticatedAndActive + HasTenantAccess **Permission**: IsAuthenticatedAndActive + HasTenantAccess
- `GET /api/v1/billing/credits/balance/balance/` - Get credit balance - `GET /api/v1/billing/credits/balance/` - Get credit balance
**Response:** **Response:**
```json ```json
@@ -1041,6 +1088,52 @@ class KeywordViewSet(SiteSectorModelViewSet):
- `start_date` - Filter by start date - `start_date` - Filter by start date
- `end_date` - Filter by end date - `end_date` - Filter by end date
#### Credit Packages
**Base Path**: `/api/v1/billing/credit-packages/`
**Permission**: IsAuthenticated
- `GET /api/v1/billing/credit-packages/` - List available credit packages
- `POST /api/v1/billing/credit-packages/{id}/purchase/` - Purchase a credit package
#### Invoices
**Base Path**: `/api/v1/billing/invoices/`
**Permission**: IsAuthenticated
- `GET /api/v1/billing/invoices/` - List invoices
- `GET /api/v1/billing/invoices/{id}/` - Get invoice detail
- `GET /api/v1/billing/invoices/{id}/download_pdf/` - Download invoice PDF
#### Payment Methods
**Base Path**: `/api/v1/billing/payment-methods/`
**Permission**: IsAuthenticated
- `GET /api/v1/billing/payment-methods/` - List payment methods
- `POST /api/v1/billing/payment-methods/` - Create payment method
- `GET /api/v1/billing/payment-methods/{id}/` - Get payment method
- `PUT /api/v1/billing/payment-methods/{id}/` - Update payment method
- `PATCH /api/v1/billing/payment-methods/{id}/` - Partial update payment method
- `DELETE /api/v1/billing/payment-methods/{id}/` - Delete payment method
- `POST /api/v1/billing/payment-methods/{id}/set_default/` - Set default payment method
- `GET /api/v1/billing/payment-methods/available/` - List available payment methods (config-driven)
#### Payments
**Base Path**: `/api/v1/billing/payments/`
**Permission**: IsAuthenticated
- `GET /api/v1/billing/payments/` - List payments
- `POST /api/v1/billing/payments/manual/` - Submit manual payment for approval
#### Transactions (alias-free)
**Base Path**: `/api/v1/billing/transactions/`
**Permission**: IsAuthenticated
- `GET /api/v1/billing/transactions/` - List transactions (with current balance included)
--- ---
## Integration Examples ## Integration Examples
@@ -1348,6 +1441,28 @@ All API changes are documented in `CHANGELOG.md` with:
- Affected areas - Affected areas
- Migration notes (if applicable) - Migration notes (if applicable)
### Endpoint Change & Documentation Checklist (Unified API)
1) Design
- Map to an existing module/tag; if new, add to Module Namespaces and Tag Map.
- Choose path under the correct base (`/api/v1/{module}/...`); avoid new sub-namespaces unless justified.
2) Implement
- Use unified response helpers and proper permissions/rate limits.
- Add `extend_schema` tags matching the module tag.
3) Schema & Docs
- Ensure swagger tag exists in `SPECTACULAR_SETTINGS` with the agreed order.
- Regenerate/reload the API (server restart) so `/api/schema/` reflects changes.
- Verify in Swagger UI (`/api/docs/`) and ReDoc (`/api/redoc/`) that the operation is under the right tag.
4) Reference Updates
- Update this reference file with the new endpoint(s) under the correct module section.
- Update `CHANGELOG.md` (type, summary, impacted clients).
5) Deprecation (if applicable)
- Mark legacy routes, add timeline, and keep compatibility shims only temporarily.
--- ---
## Summary ## Summary

View File

@@ -109,6 +109,7 @@ const endpointGroups: EndpointGroup[] = [
{ path: "/v1/system/settings/account/", method: "GET", description: "Account settings" }, { path: "/v1/system/settings/account/", method: "GET", description: "Account settings" },
{ path: "/v1/billing/credits/balance/", method: "GET", description: "Credit balance" }, { path: "/v1/billing/credits/balance/", method: "GET", description: "Credit balance" },
{ path: "/v1/billing/credits/usage/", method: "GET", description: "Usage logs" }, { path: "/v1/billing/credits/usage/", method: "GET", description: "Usage logs" },
{ path: "/v1/billing/credits/transactions/", method: "GET", description: "Transactions" },
{ path: "/v1/billing/credits/usage/summary/", method: "GET", description: "Usage summary" }, { path: "/v1/billing/credits/usage/summary/", method: "GET", description: "Usage summary" },
{ {
path: "/v1/billing/credits/usage/limits/", path: "/v1/billing/credits/usage/limits/",
@@ -126,6 +127,36 @@ const endpointGroups: EndpointGroup[] = [
{ path: "/v1/billing/credits/transactions/", method: "GET", description: "Transactions" }, { path: "/v1/billing/credits/transactions/", method: "GET", description: "Transactions" },
], ],
}, },
{
name: "Billing (Customer)",
endpoints: [
{ path: "/v1/billing/credit-packages/", method: "GET", description: "List credit packages" },
{ path: "/v1/billing/invoices/", method: "GET", description: "List invoices" },
{ path: "/v1/billing/payments/", method: "GET", description: "List payments" },
{ path: "/v1/billing/payment-methods/", method: "GET", description: "List payment methods" },
{ path: "/v1/billing/payment-methods/available/", method: "GET", description: "Available payment methods" },
{ path: "/v1/billing/payments/manual/", method: "POST", description: "Submit manual payment" },
{ path: "/v1/billing/payments/available_methods/", method: "GET", description: "Payment methods (available_methods)" },
{ path: "/v1/billing/payment-methods/1/set_default/", method: "POST", description: "Set default payment method (sample id)" },
{ path: "/v1/billing/payment-methods/1/", method: "PATCH", description: "Update payment method (sample id)" },
{ path: "/v1/billing/payment-methods/1/", method: "DELETE", description: "Delete payment method (sample id)" },
],
},
{
name: "Admin Billing",
endpoints: [
{ path: "/v1/admin/billing/stats/", method: "GET", description: "Admin billing stats" },
{ path: "/v1/admin/billing/invoices/", method: "GET", description: "Admin invoices" },
{ path: "/v1/admin/billing/payments/", method: "GET", description: "Admin payments" },
{ path: "/v1/admin/billing/pending_payments/", method: "GET", description: "Pending manual payments" },
{ path: "/v1/admin/billing/1/approve_payment/", method: "POST", description: "Approve manual payment (sample id)" },
{ path: "/v1/admin/billing/1/reject_payment/", method: "POST", description: "Reject manual payment (sample id)" },
{ path: "/v1/admin/credit-costs/", method: "GET", description: "Credit cost configs" },
{ path: "/v1/admin/credit-costs/", method: "POST", description: "Update credit cost configs" },
{ path: "/v1/admin/users/", method: "GET", description: "Admin users with credits" },
{ path: "/v1/admin/users/1/adjust-credits/", method: "POST", description: "Adjust credits (sample id)" },
],
},
{ {
name: "CRUD Operations - Planner", name: "CRUD Operations - Planner",
endpoints: [ endpoints: [

View File

@@ -193,7 +193,7 @@ export interface PendingPayment extends Payment {
export async function getCreditBalance(): Promise<CreditBalance> { export async function getCreditBalance(): Promise<CreditBalance> {
// Use business billing CreditTransactionViewSet.balance // Use business billing CreditTransactionViewSet.balance
return fetchAPI('/v1/billing/transactions/balance/'); return fetchAPI('/v1/billing/credits/balance/');
} }
export async function getCreditTransactions(): Promise<{ export async function getCreditTransactions(): Promise<{
@@ -268,7 +268,7 @@ export async function getCreditUsageLimits(): Promise<{
export async function getAdminBillingStats(): Promise<AdminBillingStats> { export async function getAdminBillingStats(): Promise<AdminBillingStats> {
// Admin billing dashboard metrics // Admin billing dashboard metrics
// Use business billing stats endpoint to include revenue, accounts, credits, and recent payments // Use business billing stats endpoint to include revenue, accounts, credits, and recent payments
return fetchAPI('/v1/billing/admin/stats/'); return fetchAPI('/v1/admin/billing/stats/');
} }
export async function getAdminInvoices(params?: { status?: string; account_id?: number; search?: string }): Promise<{ export async function getAdminInvoices(params?: { status?: string; account_id?: number; search?: string }): Promise<{
@@ -280,7 +280,7 @@ export async function getAdminInvoices(params?: { status?: string; account_id?:
if (params?.account_id) queryParams.append('account_id', String(params.account_id)); if (params?.account_id) queryParams.append('account_id', String(params.account_id));
if (params?.search) queryParams.append('search', params.search); if (params?.search) queryParams.append('search', params.search);
const url = `/v1/billing/admin/invoices/${queryParams.toString() ? '?' + queryParams.toString() : ''}`; const url = `/v1/admin/billing/invoices/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
return fetchAPI(url); return fetchAPI(url);
} }
@@ -293,7 +293,7 @@ export async function getAdminPayments(params?: { status?: string; account_id?:
if (params?.account_id) queryParams.append('account_id', String(params.account_id)); if (params?.account_id) queryParams.append('account_id', String(params.account_id));
if (params?.payment_method) queryParams.append('payment_method', params.payment_method); if (params?.payment_method) queryParams.append('payment_method', params.payment_method);
const url = `/v1/billing/admin/payments/${queryParams.toString() ? '?' + queryParams.toString() : ''}`; const url = `/v1/admin/billing/payments/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
return fetchAPI(url); return fetchAPI(url);
} }
@@ -624,7 +624,7 @@ export async function getPendingPayments(): Promise<{
results: PendingPayment[]; results: PendingPayment[];
count: number; count: number;
}> { }> {
return fetchAPI('/v1/billing/admin/pending_payments/'); return fetchAPI('/v1/admin/billing/pending_payments/');
} }
export async function approvePayment(paymentId: number, data?: { export async function approvePayment(paymentId: number, data?: {
@@ -633,7 +633,7 @@ export async function approvePayment(paymentId: number, data?: {
message: string; message: string;
payment: Payment; payment: Payment;
}> { }> {
return fetchAPI(`/v1/billing/admin/${paymentId}/approve_payment/`, { return fetchAPI(`/v1/admin/billing/${paymentId}/approve_payment/`, {
method: 'POST', method: 'POST',
body: JSON.stringify(data || {}), body: JSON.stringify(data || {}),
}); });
@@ -646,7 +646,7 @@ export async function rejectPayment(paymentId: number, data: {
message: string; message: string;
payment: Payment; payment: Payment;
}> { }> {
return fetchAPI(`/v1/billing/admin/${paymentId}/reject_payment/`, { return fetchAPI(`/v1/admin/billing/${paymentId}/reject_payment/`, {
method: 'POST', method: 'POST',
body: JSON.stringify(data), body: JSON.stringify(data),
}); });