169 lines
7.0 KiB
Python
169 lines
7.0 KiB
Python
"""
|
|
Billing Views - Payment confirmation and management
|
|
"""
|
|
from rest_framework import viewsets, status
|
|
from rest_framework.decorators import action
|
|
from django.db import transaction
|
|
from django.utils import timezone
|
|
from datetime import timedelta
|
|
from igny8_core.api.response import success_response, error_response
|
|
from igny8_core.api.permissions import IsAdminOrOwner
|
|
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
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class BillingViewSet(viewsets.GenericViewSet):
|
|
"""
|
|
ViewSet for billing operations (admin-only).
|
|
"""
|
|
permission_classes = [IsAdminOrOwner]
|
|
|
|
@action(detail=False, methods=['post'], url_path='confirm-bank-transfer')
|
|
def confirm_bank_transfer(self, request):
|
|
"""
|
|
Confirm a bank transfer payment and activate/renew subscription.
|
|
|
|
Request body:
|
|
{
|
|
"account_id": 123,
|
|
"external_payment_id": "BT-2025-001",
|
|
"amount": "29.99",
|
|
"payer_name": "John Doe",
|
|
"proof_url": "https://...",
|
|
"period_months": 1
|
|
}
|
|
"""
|
|
account_id = request.data.get('account_id')
|
|
subscription_id = request.data.get('subscription_id')
|
|
external_payment_id = request.data.get('external_payment_id')
|
|
amount = request.data.get('amount')
|
|
payer_name = request.data.get('payer_name')
|
|
proof_url = request.data.get('proof_url')
|
|
period_months = int(request.data.get('period_months', 1))
|
|
|
|
if not all([external_payment_id, amount, payer_name]):
|
|
return error_response(
|
|
error='external_payment_id, amount, and payer_name are required',
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
request=request
|
|
)
|
|
|
|
if not account_id and not subscription_id:
|
|
return error_response(
|
|
error='Either account_id or subscription_id is required',
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
request=request
|
|
)
|
|
|
|
try:
|
|
with transaction.atomic():
|
|
# Get account
|
|
if account_id:
|
|
account = Account.objects.select_related('plan').get(id=account_id)
|
|
subscription = getattr(account, 'subscription', None)
|
|
else:
|
|
subscription = Subscription.objects.select_related('account', 'account__plan').get(id=subscription_id)
|
|
account = subscription.account
|
|
|
|
if not account or not account.plan:
|
|
return error_response(
|
|
error='Account or plan not found',
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
request=request
|
|
)
|
|
|
|
# Calculate period dates based on billing cycle
|
|
now = timezone.now()
|
|
if account.plan.billing_cycle == 'monthly':
|
|
period_end = now + timedelta(days=30 * period_months)
|
|
elif account.plan.billing_cycle == 'annual':
|
|
period_end = now + timedelta(days=365 * period_months)
|
|
else:
|
|
period_end = now + timedelta(days=30 * period_months)
|
|
|
|
# Create or update subscription
|
|
if not subscription:
|
|
subscription = Subscription.objects.create(
|
|
account=account,
|
|
payment_method='bank_transfer',
|
|
external_payment_id=external_payment_id,
|
|
status='active',
|
|
current_period_start=now,
|
|
current_period_end=period_end,
|
|
cancel_at_period_end=False
|
|
)
|
|
else:
|
|
subscription.payment_method = 'bank_transfer'
|
|
subscription.external_payment_id = external_payment_id
|
|
subscription.status = 'active'
|
|
subscription.current_period_start = now
|
|
subscription.current_period_end = period_end
|
|
subscription.cancel_at_period_end = False
|
|
subscription.save()
|
|
|
|
# Update account
|
|
account.payment_method = 'bank_transfer'
|
|
account.status = 'active'
|
|
monthly_credits = account.plan.get_effective_credits_per_month()
|
|
account.credits = monthly_credits
|
|
account.save()
|
|
|
|
# Log transaction
|
|
CreditTransaction.objects.create(
|
|
account=account,
|
|
transaction_type='subscription',
|
|
amount=monthly_credits,
|
|
balance_after=monthly_credits,
|
|
description=f'Bank transfer payment confirmed: {external_payment_id}',
|
|
metadata={
|
|
'external_payment_id': external_payment_id,
|
|
'amount': str(amount),
|
|
'payer_name': payer_name,
|
|
'proof_url': proof_url if proof_url else '',
|
|
'period_months': period_months,
|
|
'confirmed_by': request.user.email
|
|
}
|
|
)
|
|
|
|
logger.info(
|
|
f'Bank transfer confirmed for account {account.id}: '
|
|
f'{external_payment_id}, {amount}, {monthly_credits} credits added'
|
|
)
|
|
|
|
return success_response(
|
|
data={
|
|
'account_id': account.id,
|
|
'subscription_id': subscription.id,
|
|
'status': 'active',
|
|
'credits': account.credits,
|
|
'period_start': subscription.current_period_start.isoformat(),
|
|
'period_end': subscription.current_period_end.isoformat()
|
|
},
|
|
message='Bank transfer confirmed successfully',
|
|
request=request
|
|
)
|
|
|
|
except Account.DoesNotExist:
|
|
return error_response(
|
|
error='Account not found',
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
request=request
|
|
)
|
|
except Subscription.DoesNotExist:
|
|
return error_response(
|
|
error='Subscription not found',
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
request=request
|
|
)
|
|
except Exception as e:
|
|
logger.error(f'Error confirming bank transfer: {str(e)}', exc_info=True)
|
|
return error_response(
|
|
error=f'Failed to confirm payment: {str(e)}',
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
request=request
|
|
)
|