diff --git a/backend/igny8_core/api/account_views.py b/backend/igny8_core/api/account_views.py index 512d0943..2b077e17 100644 --- a/backend/igny8_core/api/account_views.py +++ b/backend/igny8_core/api/account_views.py @@ -10,6 +10,7 @@ from django.contrib.auth import get_user_model from django.db.models import Q, Count, Sum from django.utils import timezone from datetime import timedelta +from drf_spectacular.utils import extend_schema, extend_schema_view from igny8_core.auth.models import Account from igny8_core.business.billing.models import CreditTransaction @@ -17,6 +18,10 @@ from igny8_core.business.billing.models import CreditTransaction User = get_user_model() +@extend_schema_view( + retrieve=extend_schema(tags=['Account']), + partial_update=extend_schema(tags=['Account']), +) class AccountSettingsViewSet(viewsets.ViewSet): """Account settings management""" 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): """Team members management""" permission_classes = [IsAuthenticated] @@ -166,6 +176,9 @@ class TeamManagementViewSet(viewsets.ViewSet): ) +@extend_schema_view( + overview=extend_schema(tags=['Account']), +) class UsageAnalyticsViewSet(viewsets.ViewSet): """Usage analytics and statistics""" permission_classes = [IsAuthenticated] diff --git a/backend/igny8_core/api/schema_extensions.py b/backend/igny8_core/api/schema_extensions.py index 2cddf15d..67c8c81b 100644 --- a/backend/igny8_core/api/schema_extensions.py +++ b/backend/igny8_core/api/schema_extensions.py @@ -8,7 +8,20 @@ from drf_spectacular.utils import extend_schema, OpenApiResponse from rest_framework import status # 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): @@ -41,6 +54,20 @@ def postprocess_schema_filter_tags(result, generator, request, public): filtered_tags = ['System'] elif '/billing/' in path or '/api/v1/billing/' in path: 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 diff --git a/backend/igny8_core/business/automation/views.py b/backend/igny8_core/business/automation/views.py index 04a4097c..18197113 100644 --- a/backend/igny8_core/business/automation/views.py +++ b/backend/igny8_core/business/automation/views.py @@ -8,6 +8,7 @@ from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated from django.shortcuts import get_object_or_404 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.services import AutomationService @@ -30,6 +31,7 @@ class AutomationViewSet(viewsets.ViewSet): site = get_object_or_404(Site, id=site_id, account=request.user.account) return site, None + @extend_schema(tags=['Automation']) @action(detail=False, methods=['get']) def config(self, request): """ @@ -68,6 +70,7 @@ class AutomationViewSet(viewsets.ViewSet): 'next_run_at': config.next_run_at, }) + @extend_schema(tags=['Automation']) @action(detail=False, methods=['put']) def update_config(self, request): """ @@ -142,6 +145,7 @@ class AutomationViewSet(viewsets.ViewSet): 'next_run_at': config.next_run_at, }) + @extend_schema(tags=['Automation']) @action(detail=False, methods=['post']) def run_now(self, request): """ @@ -175,6 +179,7 @@ class AutomationViewSet(viewsets.ViewSet): status=status.HTTP_500_INTERNAL_SERVER_ERROR ) + @extend_schema(tags=['Automation']) @action(detail=False, methods=['get']) def current_run(self, request): """ @@ -211,6 +216,7 @@ class AutomationViewSet(viewsets.ViewSet): } }) + @extend_schema(tags=['Automation']) @action(detail=False, methods=['post']) def pause(self, request): """ @@ -234,6 +240,7 @@ class AutomationViewSet(viewsets.ViewSet): status=status.HTTP_404_NOT_FOUND ) + @extend_schema(tags=['Automation']) @action(detail=False, methods=['post']) def resume(self, request): """ @@ -262,6 +269,7 @@ class AutomationViewSet(viewsets.ViewSet): status=status.HTTP_404_NOT_FOUND ) + @extend_schema(tags=['Automation']) @action(detail=False, methods=['get']) def history(self, request): """ @@ -291,6 +299,7 @@ class AutomationViewSet(viewsets.ViewSet): ] }) + @extend_schema(tags=['Automation']) @action(detail=False, methods=['get']) def logs(self, request): """ @@ -323,6 +332,7 @@ class AutomationViewSet(viewsets.ViewSet): status=status.HTTP_404_NOT_FOUND ) + @extend_schema(tags=['Automation']) @action(detail=False, methods=['get']) def estimate(self, request): """ @@ -342,6 +352,7 @@ class AutomationViewSet(viewsets.ViewSet): 'sufficient': site.account.credits >= (estimated_credits * 1.2) }) + @extend_schema(tags=['Automation']) @action(detail=False, methods=['get']) 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') def current_processing(self, request): """ @@ -547,6 +559,7 @@ class AutomationViewSet(viewsets.ViewSet): status=status.HTTP_500_INTERNAL_SERVER_ERROR ) + @extend_schema(tags=['Automation']) @action(detail=False, methods=['post'], url_path='pause') def pause_automation(self, request): """ @@ -596,6 +609,7 @@ class AutomationViewSet(viewsets.ViewSet): status=status.HTTP_500_INTERNAL_SERVER_ERROR ) + @extend_schema(tags=['Automation']) @action(detail=False, methods=['post'], url_path='resume') def resume_automation(self, request): """ @@ -649,6 +663,7 @@ class AutomationViewSet(viewsets.ViewSet): status=status.HTTP_500_INTERNAL_SERVER_ERROR ) + @extend_schema(tags=['Automation']) @action(detail=False, methods=['post'], url_path='cancel') def cancel_automation(self, request): """ diff --git a/backend/igny8_core/business/billing/urls.py b/backend/igny8_core/business/billing/urls.py index d3fd7690..3818d747 100644 --- a/backend/igny8_core/business/billing/urls.py +++ b/backend/igny8_core/business/billing/urls.py @@ -11,14 +11,21 @@ from .views import ( AdminBillingViewSet, AccountPaymentMethodViewSet, ) +from igny8_core.modules.billing.views import ( + CreditBalanceViewSet, + CreditUsageViewSet, +) router = DefaultRouter() router.register(r'invoices', InvoiceViewSet, basename='invoice') router.register(r'payments', PaymentViewSet, basename='payment') router.register(r'credit-packages', CreditPackageViewSet, basename='credit-package') router.register(r'transactions', CreditTransactionViewSet, basename='transaction') -router.register(r'admin', AdminBillingViewSet, basename='admin-billing') 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 = [ # Country/config-driven available methods (legacy alias) diff --git a/backend/igny8_core/business/billing/views.py b/backend/igny8_core/business/billing/views.py index fcadda90..73dcb4de 100644 --- a/backend/igny8_core/business/billing/views.py +++ b/backend/igny8_core/business/billing/views.py @@ -9,6 +9,7 @@ from rest_framework.permissions import IsAuthenticated from django.http import HttpResponse from django.shortcuts import get_object_or_404 from django.db import models +from drf_spectacular.utils import extend_schema, extend_schema_view from .models import ( 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): """Admin billing management""" permission_classes = [IsAuthenticated] diff --git a/backend/igny8_core/modules/billing/admin_urls.py b/backend/igny8_core/modules/billing/admin_urls.py index 9efd28e2..6ecf129f 100644 --- a/backend/igny8_core/modules/billing/admin_urls.py +++ b/backend/igny8_core/modules/billing/admin_urls.py @@ -1,6 +1,10 @@ from django.urls import path from rest_framework.routers import DefaultRouter + from .views import AdminBillingViewSet +from igny8_core.business.billing.views import ( + AdminBillingViewSet as BillingAdminViewSet, +) router = DefaultRouter() @@ -9,6 +13,12 @@ urlpatterns = [ path('users/', AdminBillingViewSet.as_view({'get': 'list_users'}), name='admin-users-list'), path('users//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'), + # 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//approve_payment/', BillingAdminViewSet.as_view({'post': 'approve_payment'}), name='admin-billing-approve-payment'), + path('billing//reject_payment/', BillingAdminViewSet.as_view({'post': 'reject_payment'}), name='admin-billing-reject-payment'), ] urlpatterns += router.urls diff --git a/backend/igny8_core/modules/billing/urls.py b/backend/igny8_core/modules/billing/urls.py index 3665f058..cb0aeda0 100644 --- a/backend/igny8_core/modules/billing/urls.py +++ b/backend/igny8_core/modules/billing/urls.py @@ -20,6 +20,9 @@ urlpatterns = [ path('', include(router.urls)), # User-facing billing overview 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('usage/', CreditUsageViewSet.as_view({'get': 'list'}), name='usage'), # Admin billing endpoints diff --git a/backend/igny8_core/modules/billing/views.py b/backend/igny8_core/modules/billing/views.py index 1d3f866a..acaa2ea2 100644 --- a/backend/igny8_core/modules/billing/views.py +++ b/backend/igny8_core/modules/billing/views.py @@ -26,7 +26,7 @@ from .exceptions import InsufficientCreditsError @extend_schema_view( - list=extend_schema(tags=['Billing']), + list=extend_schema(tags=['Billing'], summary='Get credit balance'), ) class CreditBalanceViewSet(viewsets.ViewSet): """ @@ -38,8 +38,7 @@ class CreditBalanceViewSet(viewsets.ViewSet): throttle_scope = 'billing' throttle_classes = [DebugScopedRateThrottle] - @action(detail=False, methods=['get']) - def balance(self, request): + def list(self, request): """Get current credit balance and usage""" account = getattr(request, 'account', None) if not account: @@ -125,6 +124,7 @@ class CreditUsageViewSet(AccountModelViewSet): return queryset.order_by('-created_at') + @extend_schema(tags=['Billing'], summary='Get usage summary') @action(detail=False, methods=['get']) def summary(self, request): """Get usage summary for date range""" @@ -214,6 +214,7 @@ class CreditUsageViewSet(AccountModelViewSet): serializer = UsageSummarySerializer(data) 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') def limits(self, request): """ @@ -434,6 +435,13 @@ class BillingOverviewViewSet(viewsets.ViewSet): 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): """Admin-only billing management API""" permission_classes = [IsAuthenticatedAndActive, permissions.IsAdminUser] diff --git a/backend/igny8_core/modules/integration/views.py b/backend/igny8_core/modules/integration/views.py index 10b6f12e..586c6226 100644 --- a/backend/igny8_core/modules/integration/views.py +++ b/backend/igny8_core/modules/integration/views.py @@ -6,6 +6,7 @@ from rest_framework import status from rest_framework.decorators import action from rest_framework.response import Response 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.permissions import IsAuthenticatedAndActive, IsEditorOrAbove @@ -21,6 +22,14 @@ import logging 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): """ ViewSet for SiteIntegration model. @@ -88,6 +97,7 @@ class IntegrationViewSet(SiteSectorModelViewSet): return SiteIntegrationSerializer + @extend_schema(tags=['Integration']) @action(detail=True, methods=['post']) def test_connection(self, request, pk=None): """ @@ -118,6 +128,7 @@ class IntegrationViewSet(SiteSectorModelViewSet): def allow_request(self, request, view): return True + @extend_schema(tags=['Integration']) @action(detail=False, methods=['post'], url_path='test-connection', permission_classes=[AllowAny], throttle_classes=[NoThrottle]) 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") 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']) 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 return success_response(result, request=request, status_code=response_status) + @extend_schema(tags=['Integration']) @action(detail=True, methods=['get']) def sync_status(self, request, pk=None): """ diff --git a/backend/igny8_core/modules/publisher/views.py b/backend/igny8_core/modules/publisher/views.py index f2bb3f56..1968535a 100644 --- a/backend/igny8_core/modules/publisher/views.py +++ b/backend/igny8_core/modules/publisher/views.py @@ -11,6 +11,7 @@ from rest_framework.response import Response from rest_framework.views import APIView from django.views.decorators.csrf import csrf_exempt 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.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 +@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): """ ViewSet for PublishingRecord model. @@ -41,6 +50,14 @@ class PublishingRecordViewSet(SiteSectorModelViewSet): 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): """ ViewSet for DeploymentRecord model. @@ -63,6 +80,7 @@ class DeploymentRecordViewSet(SiteSectorModelViewSet): return DeploymentRecordSerializer +@extend_schema_view() class PublisherViewSet(viewsets.ViewSet): """ Publisher actions for publishing content. @@ -76,6 +94,7 @@ class PublisherViewSet(viewsets.ViewSet): super().__init__(**kwargs) self.publisher_service = PublisherService() + @extend_schema(tags=['Publisher']) @action(detail=False, methods=['post'], url_path='publish') def publish(self, request): """ diff --git a/backend/igny8_core/modules/writer/views.py b/backend/igny8_core/modules/writer/views.py index f184fb57..76cba558 100644 --- a/backend/igny8_core/modules/writer/views.py +++ b/backend/igny8_core/modules/writer/views.py @@ -1580,12 +1580,12 @@ class ContentViewSet(SiteSectorModelViewSet): @extend_schema_view( - list=extend_schema(tags=['Writer - Taxonomies']), - create=extend_schema(tags=['Writer - Taxonomies']), - retrieve=extend_schema(tags=['Writer - Taxonomies']), - update=extend_schema(tags=['Writer - Taxonomies']), - partial_update=extend_schema(tags=['Writer - Taxonomies']), - destroy=extend_schema(tags=['Writer - Taxonomies']), + list=extend_schema(tags=['Writer']), + create=extend_schema(tags=['Writer']), + retrieve=extend_schema(tags=['Writer']), + update=extend_schema(tags=['Writer']), + partial_update=extend_schema(tags=['Writer']), + destroy=extend_schema(tags=['Writer']), ) class ContentTaxonomyViewSet(SiteSectorModelViewSet): """ diff --git a/backend/igny8_core/settings.py b/backend/igny8_core/settings.py index 21c764f8..755cfe3d 100644 --- a/backend/igny8_core/settings.py +++ b/backend/igny8_core/settings.py @@ -352,12 +352,32 @@ SPECTACULAR_SETTINGS = { # Tag configuration - prevent auto-generation and use explicit tags 'TAGS': [ {'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': 'Writer', 'description': 'Tasks, content, and images'}, - {'name': 'System', 'description': 'Settings, prompts, and integrations'}, - {'name': 'Billing', 'description': 'Credits, usage, and transactions'}, + {'name': 'Automation', 'description': 'Automation configuration and runs'}, + {'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_HOOKS': ['igny8_core.api.schema_extensions.postprocess_schema_filter_tags'], diff --git a/docs/API/API-COMPLETE-REFERENCE.md b/docs/API/API-COMPLETE-REFERENCE.md index 6ef0bc17..9c466e20 100644 --- a/docs/API/API-COMPLETE-REFERENCE.md +++ b/docs/API/API-COMPLETE-REFERENCE.md @@ -95,12 +95,35 @@ Development: http://localhost:8000/api/v1/ ``` /api/v1/ ├── 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 ├── writer/ # Tasks, content, images -├── system/ # Settings, prompts, integrations -└── billing/ # Credits, transactions, usage +├── automation/ # Automation configuration and runs +├── 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 - **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/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 **Base Path**: `/api/v1/billing/` @@ -994,7 +1041,7 @@ class KeywordViewSet(SiteSectorModelViewSet): **Base Path**: `/api/v1/billing/credits/balance/` **Permission**: IsAuthenticatedAndActive + HasTenantAccess -- `GET /api/v1/billing/credits/balance/balance/` - Get credit balance +- `GET /api/v1/billing/credits/balance/` - Get credit balance **Response:** ```json @@ -1041,6 +1088,52 @@ class KeywordViewSet(SiteSectorModelViewSet): - `start_date` - Filter by start 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 @@ -1348,6 +1441,28 @@ All API changes are documented in `CHANGELOG.md` with: - Affected areas - 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 diff --git a/frontend/src/pages/Settings/ApiMonitor.tsx b/frontend/src/pages/Settings/ApiMonitor.tsx index b05c22ef..976f73ab 100644 --- a/frontend/src/pages/Settings/ApiMonitor.tsx +++ b/frontend/src/pages/Settings/ApiMonitor.tsx @@ -109,6 +109,7 @@ const endpointGroups: EndpointGroup[] = [ { path: "/v1/system/settings/account/", method: "GET", description: "Account settings" }, { path: "/v1/billing/credits/balance/", method: "GET", description: "Credit balance" }, { 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/limits/", @@ -126,6 +127,36 @@ const endpointGroups: EndpointGroup[] = [ { 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", endpoints: [ diff --git a/frontend/src/services/billing.api.ts b/frontend/src/services/billing.api.ts index b0e021bd..922d4226 100644 --- a/frontend/src/services/billing.api.ts +++ b/frontend/src/services/billing.api.ts @@ -193,7 +193,7 @@ export interface PendingPayment extends Payment { export async function getCreditBalance(): Promise { // Use business billing CreditTransactionViewSet.balance - return fetchAPI('/v1/billing/transactions/balance/'); + return fetchAPI('/v1/billing/credits/balance/'); } export async function getCreditTransactions(): Promise<{ @@ -268,7 +268,7 @@ export async function getCreditUsageLimits(): Promise<{ export async function getAdminBillingStats(): Promise { // Admin billing dashboard metrics // 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<{ @@ -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?.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); } @@ -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?.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); } @@ -624,7 +624,7 @@ export async function getPendingPayments(): Promise<{ results: PendingPayment[]; count: number; }> { - return fetchAPI('/v1/billing/admin/pending_payments/'); + return fetchAPI('/v1/admin/billing/pending_payments/'); } export async function approvePayment(paymentId: number, data?: { @@ -633,7 +633,7 @@ export async function approvePayment(paymentId: number, data?: { message: string; payment: Payment; }> { - return fetchAPI(`/v1/billing/admin/${paymentId}/approve_payment/`, { + return fetchAPI(`/v1/admin/billing/${paymentId}/approve_payment/`, { method: 'POST', body: JSON.stringify(data || {}), }); @@ -646,7 +646,7 @@ export async function rejectPayment(paymentId: number, data: { message: string; payment: Payment; }> { - return fetchAPI(`/v1/billing/admin/${paymentId}/reject_payment/`, { + return fetchAPI(`/v1/admin/billing/${paymentId}/reject_payment/`, { method: 'POST', body: JSON.stringify(data), });