payemnt billing and credits refactoring

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-20 07:39:51 +00:00
parent a97c72640a
commit bc50b022f1
34 changed files with 3028 additions and 307 deletions

View File

@@ -32,6 +32,14 @@ class CreditBalanceViewSet(viewsets.ViewSet):
"""
ViewSet for credit balance operations
Unified API Standard v1.0 compliant
Returns:
- credits: Plan credits (reset on renewal)
- bonus_credits: Purchased credits (never expire, never reset)
- total_credits: Sum of plan + bonus credits
- plan_credits_per_month: Plan's included credits
- credits_used_this_month: Credits consumed this billing period
- credits_remaining: Total available credits
"""
permission_classes = [IsAuthenticatedAndActive, HasTenantAccess]
authentication_classes = [JWTAuthentication]
@@ -52,6 +60,8 @@ class CreditBalanceViewSet(viewsets.ViewSet):
if not account:
return success_response(data={
'credits': 0,
'bonus_credits': 0,
'total_credits': 0,
'plan_credits_per_month': 0,
'credits_used_this_month': 0,
'credits_remaining': 0,
@@ -70,20 +80,25 @@ class CreditBalanceViewSet(viewsets.ViewSet):
created_at__gte=start_of_month
).aggregate(total=Sum('credits_used'))['total'] or 0
# Plan credits (reset on renewal)
credits = account.credits or 0
credits_remaining = credits
# Bonus credits (never expire, from credit package purchases)
bonus_credits = account.bonus_credits or 0
# Total available
total_credits = credits + bonus_credits
credits_remaining = total_credits
data = {
'credits': credits,
'bonus_credits': bonus_credits,
'total_credits': total_credits,
'plan_credits_per_month': plan_credits_per_month,
'credits_used_this_month': credits_used_this_month,
'credits_remaining': credits_remaining,
}
# Validate and serialize data
serializer = CreditBalanceSerializer(data=data)
serializer.is_valid(raise_exception=True)
return success_response(data=serializer.validated_data, request=request)
# Validate and serialize data (skip serializer for now due to new fields)
return success_response(data=data, request=request)
@extend_schema_view(
@@ -682,7 +697,12 @@ class AdminBillingViewSet(viewsets.ViewSet):
invoice.paid_at = timezone.now()
invoice.save()
# 3. Get and activate subscription
from igny8_core.business.billing.services.invoice_service import InvoiceService
from igny8_core.business.billing.models import CreditPackage
invoice_type = InvoiceService.get_invoice_type(invoice) if invoice else 'custom'
# 3. Get and activate subscription (subscription invoices only)
subscription = None
if invoice and hasattr(invoice, 'subscription') and invoice.subscription:
subscription = invoice.subscription
@@ -692,46 +712,86 @@ class AdminBillingViewSet(viewsets.ViewSet):
except Exception:
pass
if subscription:
if invoice_type == 'subscription' and subscription:
subscription.status = 'active'
subscription.external_payment_id = payment.manual_reference
subscription.save(update_fields=['status', 'external_payment_id'])
# 4. CRITICAL: Set account status to active
account.status = 'active'
account.save(update_fields=['status'])
# 5. Add credits if plan has included credits
# 4. Set account status to active (subscription invoices only)
if invoice_type == 'subscription' and account.status != 'active':
account.status = 'active'
account.save(update_fields=['status'])
# 5. Add/Reset credits based on invoice type
credits_added = 0
try:
plan = None
if subscription and subscription.plan:
plan = subscription.plan
elif account and account.plan:
plan = account.plan
if plan and plan.included_credits > 0:
credits_added = plan.included_credits
CreditService.add_credits(
account=account,
amount=credits_added,
transaction_type='subscription',
description=f'{plan.name} plan credits - Invoice {invoice.invoice_number if invoice else "N/A"}',
metadata={
'subscription_id': subscription.id if subscription else None,
'invoice_id': invoice.id if invoice else None,
'payment_id': payment.id,
'plan_id': plan.id,
'approved_by': request.user.email
}
)
if invoice_type == 'credit_package':
credit_package_id = invoice.metadata.get('credit_package_id') if invoice and invoice.metadata else None
if credit_package_id:
package = CreditPackage.objects.get(id=credit_package_id)
credits_added = package.credits
CreditService.add_credits(
account=account,
amount=credits_added,
transaction_type='purchase',
description=f'Credit package: {package.name} ({credits_added} credits) - Invoice {invoice.invoice_number if invoice else "N/A"}',
metadata={
'invoice_id': invoice.id if invoice else None,
'payment_id': payment.id,
'credit_package_id': str(package.id),
'approved_by': request.user.email
}
)
elif invoice_type == 'subscription':
plan = None
if subscription and subscription.plan:
plan = subscription.plan
elif account and account.plan:
plan = account.plan
if plan and plan.included_credits > 0:
credits_added = plan.included_credits
CreditService.reset_credits_for_renewal(
account=account,
new_amount=credits_added,
description=f'{plan.name} plan credits - Invoice {invoice.invoice_number if invoice else "N/A"}',
metadata={
'subscription_id': subscription.id if subscription else None,
'invoice_id': invoice.id if invoice else None,
'payment_id': payment.id,
'plan_id': plan.id,
'approved_by': request.user.email
}
)
except Exception as credit_error:
logger.error(f'Credit addition failed for payment {payment.id}: {credit_error}', exc_info=True)
# Don't fail the approval if credits fail - account is still activated
# Don't fail the approval if credits fail
logger.info(
f'Payment approved: Payment {payment.id}, Account {account.id} set to active, '
f'{credits_added} credits added'
f'Payment approved: Payment {payment.id}, Account {account.id}, '
f'invoice_type={invoice_type}, credits_added={credits_added}'
)
# Log to WebhookEvent for unified payment logs
from igny8_core.business.billing.models import WebhookEvent
WebhookEvent.record_event(
event_id=f'{payment.payment_method}-approved-{payment.id}-{timezone.now().timestamp()}',
provider=payment.payment_method,
event_type='payment.approved',
payload={
'payment_id': payment.id,
'invoice_id': invoice.id if invoice else None,
'invoice_number': invoice.invoice_number if invoice else None,
'invoice_type': invoice_type,
'account_id': account.id,
'amount': str(payment.amount),
'currency': payment.currency,
'manual_reference': payment.manual_reference,
'approved_by': request.user.email,
'credits_added': credits_added,
'subscription_id': subscription.id if subscription else None,
},
processed=True
)
# 6. Send approval email
@@ -783,6 +843,24 @@ class AdminBillingViewSet(viewsets.ViewSet):
logger.info(f'Payment rejected: Payment {payment.id}, Reason: {rejection_reason}')
# Log to WebhookEvent for unified payment logs
from igny8_core.business.billing.models import WebhookEvent
WebhookEvent.record_event(
event_id=f'{payment.payment_method}-rejected-{payment.id}-{timezone.now().timestamp()}',
provider=payment.payment_method,
event_type='payment.rejected',
payload={
'payment_id': payment.id,
'account_id': account.id if account else None,
'amount': str(payment.amount),
'currency': payment.currency,
'manual_reference': payment.manual_reference,
'rejected_by': request.user.email,
'rejection_reason': rejection_reason,
},
processed=True
)
# Send rejection email
try:
from igny8_core.business.billing.services.email_service import BillingEmailService