many fixes

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-06 14:31:42 +00:00
parent 4a16a6a402
commit c455a5ad83
21 changed files with 1497 additions and 242 deletions

View File

@@ -3,7 +3,7 @@ Billing Business Logic Admin
"""
from django.contrib import admin
from django.utils.html import format_html
from .models import CreditCostConfig
from .models import CreditCostConfig, AccountPaymentMethod
@admin.register(CreditCostConfig)
@@ -79,3 +79,32 @@ class CreditCostConfigAdmin(admin.ModelAdmin):
"""Track who made the change"""
obj.updated_by = request.user
super().save_model(request, obj, form, change)
@admin.register(AccountPaymentMethod)
class AccountPaymentMethodAdmin(admin.ModelAdmin):
list_display = [
'display_name',
'type',
'account',
'is_default',
'is_enabled',
'country_code',
'is_verified',
'updated_at',
]
list_filter = ['type', 'is_default', 'is_enabled', 'is_verified', 'country_code']
search_fields = ['display_name', 'account__name', 'account__id']
readonly_fields = ['created_at', 'updated_at']
fieldsets = (
('Payment Method', {
'fields': ('account', 'type', 'display_name', 'is_default', 'is_enabled', 'is_verified', 'country_code')
}),
('Instructions / Metadata', {
'fields': ('instructions', 'metadata')
}),
('Timestamps', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)

View File

@@ -457,3 +457,43 @@ class PaymentMethodConfig(models.Model):
def __str__(self):
return f"{self.country_code} - {self.get_payment_method_display()}"
class AccountPaymentMethod(AccountBaseModel):
"""
Account-scoped payment methods (Stripe/PayPal/manual bank/wallet).
Only metadata/refs are stored here; no secrets.
"""
PAYMENT_METHOD_CHOICES = [
('stripe', 'Stripe'),
('paypal', 'PayPal'),
('bank_transfer', 'Bank Transfer'),
('local_wallet', 'Local Wallet'),
]
type = models.CharField(max_length=50, choices=PAYMENT_METHOD_CHOICES, db_index=True)
display_name = models.CharField(max_length=100, help_text="User-visible label", default='')
is_default = models.BooleanField(default=False, db_index=True)
is_enabled = models.BooleanField(default=True, db_index=True)
is_verified = models.BooleanField(default=False, db_index=True)
country_code = models.CharField(max_length=2, blank=True, default='', help_text="ISO-2 country code (optional)")
# Manual/bank/local wallet details (non-sensitive metadata)
instructions = models.TextField(blank=True, default='')
metadata = models.JSONField(default=dict, blank=True, help_text="Provider references or display metadata")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
app_label = 'billing'
db_table = 'igny8_account_payment_methods'
ordering = ['-is_default', 'display_name', 'id']
indexes = [
models.Index(fields=['account', 'is_default']),
models.Index(fields=['account', 'type']),
]
unique_together = [['account', 'display_name']]
def __str__(self):
return f"{self.account_id} - {self.display_name} ({self.type})"

View File

@@ -8,7 +8,8 @@ from .views import (
PaymentViewSet,
CreditPackageViewSet,
CreditTransactionViewSet,
AdminBillingViewSet
AdminBillingViewSet,
AccountPaymentMethodViewSet,
)
router = DefaultRouter()
@@ -17,9 +18,10 @@ 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')
urlpatterns = [
# Payment methods alias for easier frontend access
path('payment-methods/', PaymentViewSet.as_view({'get': 'available_methods'}), name='payment-methods'),
# Country/config-driven available methods (legacy alias)
path('payment-methods/available/', PaymentViewSet.as_view({'get': 'available_methods'}), name='payment-methods-available'),
path('', include(router.urls)),
]

View File

@@ -2,7 +2,7 @@
Billing API Views
Comprehensive billing endpoints for invoices, payments, credit packages
"""
from rest_framework import viewsets, status
from rest_framework import viewsets, status, serializers
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
@@ -10,7 +10,14 @@ from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.db import models
from .models import Invoice, Payment, CreditPackage, PaymentMethodConfig, CreditTransaction
from .models import (
Invoice,
Payment,
CreditPackage,
PaymentMethodConfig,
CreditTransaction,
AccountPaymentMethod,
)
from .services.invoice_service import InvoiceService
from .services.payment_service import PaymentService
@@ -172,6 +179,69 @@ class PaymentViewSet(viewsets.ViewSet):
}, status=status.HTTP_201_CREATED)
class AccountPaymentMethodSerializer(serializers.ModelSerializer):
class Meta:
model = AccountPaymentMethod
fields = [
'id',
'type',
'display_name',
'is_default',
'is_enabled',
'is_verified',
'country_code',
'instructions',
'metadata',
'created_at',
'updated_at',
]
read_only_fields = ['id', 'is_verified', 'created_at', 'updated_at']
class AccountPaymentMethodViewSet(viewsets.ModelViewSet):
"""
CRUD for account-scoped payment methods (Stripe/PayPal/manual bank/local_wallet).
"""
serializer_class = AccountPaymentMethodSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
account = getattr(self.request.user, 'account', None)
qs = AccountPaymentMethod.objects.all()
if account:
qs = qs.filter(account=account)
else:
qs = qs.none()
return qs.order_by('-is_default', 'display_name', 'id')
def perform_create(self, serializer):
account = self.request.user.account
with models.transaction.atomic():
obj = serializer.save(account=account)
make_default = serializer.validated_data.get('is_default') or not AccountPaymentMethod.objects.filter(account=account, is_default=True).exists()
if make_default:
AccountPaymentMethod.objects.filter(account=account).exclude(id=obj.id).update(is_default=False)
obj.is_default = True
obj.save(update_fields=['is_default'])
def perform_update(self, serializer):
account = self.request.user.account
with models.transaction.atomic():
obj = serializer.save()
if serializer.validated_data.get('is_default'):
AccountPaymentMethod.objects.filter(account=account).exclude(id=obj.id).update(is_default=False)
@action(detail=True, methods=['post'])
def set_default(self, request, pk=None):
account = request.user.account
method = get_object_or_404(AccountPaymentMethod, id=pk, account=account)
with models.transaction.atomic():
AccountPaymentMethod.objects.filter(account=account).update(is_default=False)
method.is_default = True
method.save(update_fields=['is_default'])
return Response({'message': 'Default payment method updated', 'id': method.id})
class CreditPackageViewSet(viewsets.ViewSet):
"""Credit package endpoints"""
permission_classes = [IsAuthenticated]
@@ -485,14 +555,15 @@ class AdminBillingViewSet(viewsets.ViewSet):
# Account stats
total_accounts = Account.objects.count()
active_accounts = Account.objects.filter(is_active=True).count()
active_accounts = Account.objects.filter(status='active').count()
new_accounts_this_month = Account.objects.filter(
created_at__gte=this_month_start
).count()
# Subscription stats
# Subscriptions are linked via OneToOne "subscription"
active_subscriptions = Account.objects.filter(
subscriptions__status='active'
subscription__status='active'
).distinct().count()
# Revenue stats
@@ -513,8 +584,8 @@ class AdminBillingViewSet(viewsets.ViewSet):
created_at__gte=last_30_days
).aggregate(total=Sum('amount'))['total'] or 0
# Usage transactions are stored as deductions (negative amounts)
credits_used = abs(CreditTransaction.objects.filter(
transaction_type__in=['generate_content', 'keyword_research', 'ai_task'],
created_at__gte=last_30_days,
amount__lt=0
).aggregate(total=Sum('amount'))['total'] or 0)
@@ -533,18 +604,20 @@ class AdminBillingViewSet(viewsets.ViewSet):
status__in=['completed', 'succeeded']
).order_by('-processed_at')[:5]
recent_activity = [
{
recent_activity = []
for pay in recent_payments:
account_name = getattr(pay.account, 'name', 'Unknown')
currency = pay.currency or 'USD'
ts = pay.processed_at.isoformat() if pay.processed_at else now.isoformat()
recent_activity.append({
'id': pay.id,
'type': 'payment',
'account_name': pay.account.name,
'account_name': account_name,
'amount': str(pay.amount),
'currency': pay.currency,
'timestamp': pay.processed_at.isoformat(),
'description': f'Payment received via {pay.payment_method}'
}
for pay in recent_payments
]
'currency': currency,
'timestamp': ts,
'description': f'Payment received via {pay.payment_method or "unknown"}'
})
return Response({
'total_accounts': total_accounts,