Complete Implemenation of tenancy
This commit is contained in:
@@ -4,7 +4,9 @@ Billing Views - Payment confirmation and management
|
||||
from rest_framework import viewsets, status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import AllowAny
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
from django.http import HttpResponse
|
||||
from datetime import timedelta
|
||||
@@ -15,7 +17,11 @@ from igny8_core.api.pagination import CustomPageNumberPagination
|
||||
from igny8_core.auth.models import Account, Subscription
|
||||
from igny8_core.business.billing.services.credit_service import CreditService
|
||||
from igny8_core.business.billing.services.invoice_service import InvoiceService
|
||||
from igny8_core.business.billing.models import CreditTransaction, Invoice, Payment, CreditPackage, AccountPaymentMethod
|
||||
from igny8_core.business.billing.models import (
|
||||
CreditTransaction, Invoice, Payment, CreditPackage,
|
||||
AccountPaymentMethod, PaymentMethodConfig
|
||||
)
|
||||
from igny8_core.modules.billing.serializers import PaymentMethodConfigSerializer, PaymentConfirmationSerializer
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -171,6 +177,299 @@ class BillingViewSet(viewsets.GenericViewSet):
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
|
||||
@action(detail=False, methods=['get'], url_path='payment-methods', permission_classes=[AllowAny])
|
||||
def list_payment_methods(self, request):
|
||||
"""
|
||||
Get available payment methods for a specific country.
|
||||
|
||||
Query params:
|
||||
country: ISO 2-letter country code (default: '*' for global)
|
||||
|
||||
Returns payment methods filtered by country (country-specific + global).
|
||||
"""
|
||||
country = request.GET.get('country', '*').upper()
|
||||
|
||||
# Get country-specific + global methods
|
||||
methods = PaymentMethodConfig.objects.filter(
|
||||
Q(country_code=country) | Q(country_code='*'),
|
||||
is_enabled=True
|
||||
).order_by('sort_order')
|
||||
|
||||
serializer = PaymentMethodConfigSerializer(methods, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=False, methods=['post'], url_path='payments/confirm', permission_classes=[IsAuthenticatedAndActive])
|
||||
def confirm_payment(self, request):
|
||||
"""
|
||||
User confirms manual payment (bank transfer or local wallet).
|
||||
Creates Payment record with status='pending_approval' for admin review.
|
||||
|
||||
Request body:
|
||||
{
|
||||
"invoice_id": 123,
|
||||
"payment_method": "bank_transfer",
|
||||
"manual_reference": "BT-20251208-12345",
|
||||
"manual_notes": "Transferred via ABC Bank",
|
||||
"amount": "29.00",
|
||||
"proof_url": "https://..." // optional
|
||||
}
|
||||
"""
|
||||
serializer = PaymentConfirmationSerializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
return error_response(
|
||||
error=serializer.errors,
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
invoice_id = serializer.validated_data['invoice_id']
|
||||
payment_method = serializer.validated_data['payment_method']
|
||||
manual_reference = serializer.validated_data['manual_reference']
|
||||
manual_notes = serializer.validated_data.get('manual_notes', '')
|
||||
amount = serializer.validated_data['amount']
|
||||
proof_url = serializer.validated_data.get('proof_url')
|
||||
|
||||
try:
|
||||
# Get invoice - must belong to user's account
|
||||
invoice = Invoice.objects.select_related('account').get(
|
||||
id=invoice_id,
|
||||
account=request.account
|
||||
)
|
||||
|
||||
# Validate amount matches invoice
|
||||
if amount != invoice.total:
|
||||
return error_response(
|
||||
error=f'Amount mismatch. Invoice total is {invoice.total} {invoice.currency}',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
# Create payment record with pending approval status
|
||||
payment = Payment.objects.create(
|
||||
account=request.account,
|
||||
invoice=invoice,
|
||||
amount=amount,
|
||||
currency=invoice.currency,
|
||||
status='pending_approval',
|
||||
payment_method=payment_method,
|
||||
manual_reference=manual_reference,
|
||||
manual_notes=manual_notes,
|
||||
metadata={'proof_url': proof_url, 'submitted_by': request.user.email} if proof_url else {'submitted_by': request.user.email}
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f'Payment confirmation submitted: Payment {payment.id}, '
|
||||
f'Invoice {invoice.invoice_number}, Account {request.account.id}, '
|
||||
f'Reference: {manual_reference}'
|
||||
)
|
||||
|
||||
# TODO: Send notification to admin
|
||||
# send_payment_confirmation_notification(payment)
|
||||
|
||||
return success_response(
|
||||
data={
|
||||
'payment_id': payment.id,
|
||||
'invoice_id': invoice.id,
|
||||
'invoice_number': invoice.invoice_number,
|
||||
'status': 'pending_approval',
|
||||
'amount': str(amount),
|
||||
'currency': invoice.currency,
|
||||
'manual_reference': manual_reference
|
||||
},
|
||||
message='Payment confirmation submitted for review. You will be notified once approved.',
|
||||
request=request
|
||||
)
|
||||
|
||||
except Invoice.DoesNotExist:
|
||||
return error_response(
|
||||
error='Invoice not found or does not belong to your account',
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
request=request
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'Error confirming payment: {str(e)}', exc_info=True)
|
||||
return error_response(
|
||||
error=f'Failed to submit payment confirmation: {str(e)}',
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
|
||||
@action(detail=True, methods=['post'], url_path='approve', permission_classes=[IsAdminOrOwner])
|
||||
def approve_payment(self, request, pk=None):
|
||||
"""
|
||||
Admin approves a manual payment.
|
||||
Atomically updates: payment status → invoice paid → subscription active → account active → add credits.
|
||||
|
||||
Request body:
|
||||
{
|
||||
"admin_notes": "Verified payment in bank statement"
|
||||
}
|
||||
"""
|
||||
admin_notes = request.data.get('admin_notes', '')
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
# Get payment with related objects
|
||||
payment = Payment.objects.select_related(
|
||||
'invoice',
|
||||
'invoice__subscription',
|
||||
'invoice__subscription__plan',
|
||||
'account'
|
||||
).get(id=pk)
|
||||
|
||||
if payment.status != 'pending_approval':
|
||||
return error_response(
|
||||
error=f'Payment is not pending approval (current status: {payment.status})',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
invoice = payment.invoice
|
||||
subscription = invoice.subscription
|
||||
account = payment.account
|
||||
|
||||
# 1. Update Payment
|
||||
payment.status = 'succeeded'
|
||||
payment.approved_by = request.user
|
||||
payment.approved_at = timezone.now()
|
||||
payment.processed_at = timezone.now()
|
||||
payment.admin_notes = admin_notes
|
||||
payment.save(update_fields=['status', 'approved_by', 'approved_at', 'processed_at', 'admin_notes'])
|
||||
|
||||
# 2. Update Invoice
|
||||
invoice.status = 'paid'
|
||||
invoice.paid_at = timezone.now()
|
||||
invoice.save(update_fields=['status', 'paid_at'])
|
||||
|
||||
# 3. Update Subscription
|
||||
if subscription:
|
||||
subscription.status = 'active'
|
||||
subscription.external_payment_id = payment.manual_reference
|
||||
subscription.save(update_fields=['status', 'external_payment_id'])
|
||||
|
||||
# 4. Update Account
|
||||
account.status = 'active'
|
||||
account.save(update_fields=['status'])
|
||||
|
||||
# 5. Add Credits (if subscription has plan)
|
||||
credits_added = 0
|
||||
if subscription and subscription.plan:
|
||||
credits_added = subscription.plan.included_credits
|
||||
|
||||
# Use CreditService to add credits
|
||||
CreditService.add_credits(
|
||||
account=account,
|
||||
amount=credits_added,
|
||||
transaction_type='subscription',
|
||||
description=f'{subscription.plan.name} plan credits - Invoice {invoice.invoice_number}',
|
||||
metadata={
|
||||
'subscription_id': subscription.id,
|
||||
'invoice_id': invoice.id,
|
||||
'payment_id': payment.id,
|
||||
'plan_id': subscription.plan.id,
|
||||
'approved_by': request.user.email
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f'Payment approved: Payment {payment.id}, Invoice {invoice.invoice_number}, '
|
||||
f'Account {account.id} activated, {credits_added} credits added'
|
||||
)
|
||||
|
||||
# TODO: Send activation email to user
|
||||
# send_account_activated_email(account, subscription)
|
||||
|
||||
return success_response(
|
||||
data={
|
||||
'payment_id': payment.id,
|
||||
'invoice_id': invoice.id,
|
||||
'invoice_number': invoice.invoice_number,
|
||||
'account_id': account.id,
|
||||
'account_status': account.status,
|
||||
'subscription_status': subscription.status if subscription else None,
|
||||
'credits_added': credits_added,
|
||||
'total_credits': account.credits,
|
||||
'approved_by': request.user.email,
|
||||
'approved_at': payment.approved_at.isoformat()
|
||||
},
|
||||
message='Payment approved successfully. Account activated.',
|
||||
request=request
|
||||
)
|
||||
|
||||
except Payment.DoesNotExist:
|
||||
return error_response(
|
||||
error='Payment not found',
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
request=request
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'Error approving payment: {str(e)}', exc_info=True)
|
||||
return error_response(
|
||||
error=f'Failed to approve payment: {str(e)}',
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
|
||||
@action(detail=True, methods=['post'], url_path='reject', permission_classes=[IsAdminOrOwner])
|
||||
def reject_payment(self, request, pk=None):
|
||||
"""
|
||||
Admin rejects a manual payment.
|
||||
|
||||
Request body:
|
||||
{
|
||||
"admin_notes": "Transaction reference not found in bank statement"
|
||||
}
|
||||
"""
|
||||
admin_notes = request.data.get('admin_notes', 'Payment rejected by admin')
|
||||
|
||||
try:
|
||||
payment = Payment.objects.get(id=pk)
|
||||
|
||||
if payment.status != 'pending_approval':
|
||||
return error_response(
|
||||
error=f'Payment is not pending approval (current status: {payment.status})',
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
request=request
|
||||
)
|
||||
|
||||
payment.status = 'failed'
|
||||
payment.approved_by = request.user
|
||||
payment.approved_at = timezone.now()
|
||||
payment.failed_at = timezone.now()
|
||||
payment.admin_notes = admin_notes
|
||||
payment.failure_reason = admin_notes
|
||||
payment.save(update_fields=['status', 'approved_by', 'approved_at', 'failed_at', 'admin_notes', 'failure_reason'])
|
||||
|
||||
logger.info(f'Payment rejected: Payment {payment.id}, Reason: {admin_notes}')
|
||||
|
||||
# TODO: Send rejection email to user
|
||||
# send_payment_rejected_email(payment)
|
||||
|
||||
return success_response(
|
||||
data={
|
||||
'payment_id': payment.id,
|
||||
'status': 'failed',
|
||||
'rejected_by': request.user.email,
|
||||
'rejected_at': payment.approved_at.isoformat()
|
||||
},
|
||||
message='Payment rejected.',
|
||||
request=request
|
||||
)
|
||||
|
||||
except Payment.DoesNotExist:
|
||||
return error_response(
|
||||
error='Payment not found',
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
request=request
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'Error rejecting payment: {str(e)}', exc_info=True)
|
||||
return error_response(
|
||||
error=f'Failed to reject payment: {str(e)}',
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
request=request
|
||||
)
|
||||
|
||||
|
||||
class InvoiceViewSet(AccountModelViewSet):
|
||||
|
||||
Reference in New Issue
Block a user