fixes fixes fixes tenaancy

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-09 02:43:51 +00:00
parent 92211f065b
commit 72d0b6b0fd
23 changed files with 1428 additions and 19 deletions

View File

@@ -1,12 +1,113 @@
"""
Admin interface for auth models
"""
from django import forms
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from igny8_core.admin.base import AccountAdminMixin
from .models import User, Account, Plan, Subscription, Site, Sector, SiteUserAccess, Industry, IndustrySector, SeedKeyword, PasswordResetToken
class AccountAdminForm(forms.ModelForm):
"""Custom form for Account admin with dynamic payment method choices from PaymentMethodConfig"""
class Meta:
model = Account
fields = '__all__'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
from igny8_core.business.billing.models import PaymentMethodConfig, AccountPaymentMethod
if self.instance and self.instance.pk:
# Get country from billing_country, fallback to wildcard '*' for global
country = self.instance.billing_country or '*'
# Get enabled payment methods for this country OR global (*)
available_methods = PaymentMethodConfig.objects.filter(
country_code__in=[country, '*'],
is_enabled=True
).order_by('country_code', 'sort_order').values_list('payment_method', 'display_name')
if available_methods:
# Build choices from PaymentMethodConfig
choices = []
seen = set()
for method_type, display_name in available_methods:
if method_type not in seen:
choices.append((method_type, display_name or method_type.replace('_', ' ').title()))
seen.add(method_type)
else:
# Fallback to model choices if no configs
choices = Account.PAYMENT_METHOD_CHOICES
self.fields['payment_method'].widget = forms.Select(choices=choices)
# Get current default from AccountPaymentMethod
default_method = AccountPaymentMethod.objects.filter(
account=self.instance,
is_default=True,
is_enabled=True
).first()
if default_method:
self.fields['payment_method'].initial = default_method.type
self.fields['payment_method'].help_text = f'✓ Current: {default_method.display_name} ({default_method.get_type_display()})'
else:
self.fields['payment_method'].help_text = 'Select from available payment methods based on country'
def save(self, commit=True):
"""When payment_method changes, update/create AccountPaymentMethod"""
from igny8_core.business.billing.models import AccountPaymentMethod, PaymentMethodConfig
instance = super().save(commit=False)
if commit:
instance.save()
# Get selected payment method
selected_type = self.cleaned_data.get('payment_method')
if selected_type:
# Get config for display name and instructions
country = instance.billing_country or '*'
config = PaymentMethodConfig.objects.filter(
country_code__in=[country, '*'],
payment_method=selected_type,
is_enabled=True
).first()
# Create or update AccountPaymentMethod
account_method, created = AccountPaymentMethod.objects.get_or_create(
account=instance,
type=selected_type,
defaults={
'display_name': config.display_name if config else selected_type.replace('_', ' ').title(),
'is_default': True,
'is_enabled': True,
'instructions': config.instructions if config else '',
'country_code': instance.billing_country or '',
}
)
if not created:
# Update existing and set as default
account_method.is_default = True
account_method.is_enabled = True
if config:
account_method.display_name = config.display_name
account_method.instructions = config.instructions
account_method.save()
# Unset other methods as default
AccountPaymentMethod.objects.filter(
account=instance
).exclude(id=account_method.id).update(is_default=False)
return instance
@admin.register(Plan)
class PlanAdmin(admin.ModelAdmin):
"""Plan admin - Global, no account filtering needed"""
@@ -33,6 +134,7 @@ class PlanAdmin(admin.ModelAdmin):
@admin.register(Account)
class AccountAdmin(AccountAdminMixin, admin.ModelAdmin):
form = AccountAdminForm
list_display = ['name', 'slug', 'owner', 'plan', 'status', 'credits', 'created_at']
list_filter = ['status', 'plan']
search_fields = ['name', 'slug']

View File

@@ -108,6 +108,11 @@ class InvoiceAdmin(AccountAdminMixin, admin.ModelAdmin):
@admin.register(Payment)
class PaymentAdmin(AccountAdminMixin, admin.ModelAdmin):
\"\"\"
Payment admin - DO NOT USE.
Use the Payment admin in modules/billing/admin.py which has approval workflow actions.
This is kept for backward compatibility only.
\"\"\"
list_display = [
'id',
'invoice',
@@ -121,6 +126,9 @@ class PaymentAdmin(AccountAdminMixin, admin.ModelAdmin):
list_filter = ['status', 'payment_method', 'currency', 'created_at']
search_fields = ['invoice__invoice_number', 'account__name', 'stripe_payment_intent_id', 'paypal_order_id']
readonly_fields = ['created_at', 'updated_at']
def has_add_permission(self, request):\n return False # Prevent creating payments here
\n def has_delete_permission(self, request, obj=None):\n return False # Prevent deleting payments here
@admin.register(CreditPackage)

View File

@@ -0,0 +1,35 @@
# Generated migration for Payment status simplification
from django.db import migrations
def migrate_payment_statuses(apps, schema_editor):
"""
Migrate old payment statuses to new simplified statuses:
- pending, processing, completed, cancelled → map to new statuses
"""
Payment = apps.get_model('billing', 'Payment')
# Map old statuses to new statuses
status_mapping = {
'pending': 'pending_approval', # Treat as pending approval
'processing': 'pending_approval', # Treat as pending approval
'completed': 'succeeded', # completed = succeeded
'cancelled': 'failed', # cancelled = failed
# Keep existing: pending_approval, succeeded, failed, refunded
}
for old_status, new_status in status_mapping.items():
Payment.objects.filter(status=old_status).update(status=new_status)
class Migration(migrations.Migration):
dependencies = [
('billing', '0006_auto_20251209_payment_workflow'), # Adjust to your latest migration
]
operations = [
# Update status choices (Django will handle this in model)
migrations.RunPython(migrate_payment_statuses, reverse_code=migrations.RunPython.noop),
]

View File

@@ -240,12 +240,16 @@ class Invoice(AccountBaseModel):
@property
def billing_period_start(self):
"""Get from subscription - single source of truth"""
return self.subscription.current_period_start if self.subscription else None
if self.account and hasattr(self.account, 'subscription'):
return self.account.subscription.current_period_start
return None
@property
def billing_period_end(self):
"""Get from subscription - single source of truth"""
return self.subscription.current_period_end if self.subscription else None
if self.account and hasattr(self.account, 'subscription'):
return self.account.subscription.current_period_end
return None
@property
def billing_email(self):
@@ -285,14 +289,10 @@ class Payment(AccountBaseModel):
Supports: Stripe, PayPal, Manual (Bank Transfer, Local Wallet)
"""
STATUS_CHOICES = [
('pending', 'Pending'),
('pending_approval', 'Pending Approval'),
('processing', 'Processing'),
('succeeded', 'Succeeded'),
('completed', 'Completed'), # Legacy alias for succeeded
('failed', 'Failed'),
('refunded', 'Refunded'),
('cancelled', 'Cancelled'),
('pending_approval', 'Pending Approval'), # Manual payment submitted by user
('succeeded', 'Succeeded'), # Payment approved and processed
('failed', 'Failed'), # Payment rejected or failed
('refunded', 'Refunded'), # Payment refunded (rare)
]
PAYMENT_METHOD_CHOICES = [
@@ -366,6 +366,85 @@ class Payment(AccountBaseModel):
def __str__(self):
return f"Payment {self.id} - {self.get_payment_method_display()} - {self.amount} {self.currency}"
def save(self, *args, **kwargs):
"""
Override save to automatically update related objects when payment is approved.
When status changes to 'succeeded', automatically:
1. Mark invoice as paid
2. Activate subscription
3. Activate account
4. Add credits
"""
# Check if status is changing to succeeded
is_new = self.pk is None
old_status = None
if not is_new:
try:
old_payment = Payment.objects.get(pk=self.pk)
old_status = old_payment.status
except Payment.DoesNotExist:
pass
# If status is changing to succeeded, trigger approval workflow
if self.status == 'succeeded' and old_status != 'succeeded':
from django.utils import timezone
from django.db import transaction
from igny8_core.business.billing.services.credit_service import CreditService
# Set approval timestamp if not set
if not self.processed_at:
self.processed_at = timezone.now()
if not self.approved_at:
self.approved_at = timezone.now()
# Save payment first
super().save(*args, **kwargs)
# Then update related objects in transaction
with transaction.atomic():
# 1. Update Invoice
if self.invoice:
self.invoice.status = 'paid'
self.invoice.paid_at = timezone.now()
self.invoice.save(update_fields=['status', 'paid_at'])
# 2. Update Account (MUST be before subscription check)
if self.account:
self.account.status = 'active'
self.account.save(update_fields=['status'])
# 3. Update Subscription via account.subscription (one-to-one relationship)
try:
if hasattr(self.account, 'subscription'):
subscription = self.account.subscription
subscription.status = 'active'
subscription.external_payment_id = self.manual_reference or f'payment-{self.id}'
subscription.save(update_fields=['status', 'external_payment_id'])
# 4. Add Credits from subscription plan
if subscription.plan and subscription.plan.included_credits > 0:
CreditService.add_credits(
account=self.account,
amount=subscription.plan.included_credits,
transaction_type='subscription',
description=f'{subscription.plan.name} - Invoice {self.invoice.invoice_number}',
metadata={
'subscription_id': subscription.id,
'invoice_id': self.invoice.id,
'payment_id': self.id,
'auto_approved': True
}
)
except Exception as e:
# Log error but don't fail payment save
import logging
logger = logging.getLogger(__name__)
logger.error(f'Error updating subscription/credits for payment {self.id}: {e}', exc_info=True)
else:
# Normal save
super().save(*args, **kwargs)
class CreditPackage(models.Model):

View File

@@ -25,6 +25,7 @@ router.register(r'invoices', InvoiceViewSet, basename='invoices')
router.register(r'payments', PaymentViewSet, basename='payments')
router.register(r'credit-packages', CreditPackageViewSet, basename='credit-packages')
router.register(r'payment-methods', AccountPaymentMethodViewSet, basename='payment-methods')
router.register(r'payment-configs', BillingViewSet, basename='payment-configs')
urlpatterns = [
path('', include(router.urls)),

View File

@@ -68,6 +68,14 @@ class InvoiceAdmin(AccountAdminMixin, admin.ModelAdmin):
@admin.register(Payment)
class PaymentAdmin(AccountAdminMixin, admin.ModelAdmin):
"""
Main Payment Admin with approval workflow.
When you change status to 'succeeded', it automatically:
- Updates invoice to 'paid'
- Activates subscription
- Activates account
- Adds credits
"""
list_display = [
'id',
'invoice',
@@ -93,6 +101,37 @@ class PaymentAdmin(AccountAdminMixin, admin.ModelAdmin):
readonly_fields = ['created_at', 'updated_at', 'approved_at', 'processed_at', 'failed_at', 'refunded_at']
actions = ['approve_payments', 'reject_payments']
fieldsets = (
('Payment Info', {
'fields': ('invoice', 'account', 'amount', 'currency', 'payment_method', 'status')
}),
('Manual Payment Details', {
'fields': ('manual_reference', 'manual_notes', 'admin_notes'),
'classes': ('collapse',),
}),
('Stripe/PayPal', {
'fields': ('stripe_payment_intent_id', 'stripe_charge_id', 'paypal_order_id', 'paypal_capture_id'),
'classes': ('collapse',),
}),
('Approval Info', {
'fields': ('approved_by', 'approved_at', 'processed_at', 'failed_at', 'refunded_at', 'failure_reason'),
'classes': ('collapse',),
}),
('Timestamps', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',),
}),
)
def save_model(self, request, obj, form, change):
"""
Override save_model to set approved_by when status changes to succeeded.
The Payment.save() method will handle all the cascade updates automatically.
"""
if obj.status == 'succeeded' and not obj.approved_by:
obj.approved_by = request.user
super().save_model(request, obj, form, change)
def approve_payments(self, request, queryset):
"""Approve selected manual payments"""
from django.db import transaction

View File

@@ -0,0 +1,28 @@
# Generated by GitHub Copilot on 2025-12-09 02:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('billing', '0006_accountpaymentmethod'),
]
operations = [
migrations.AlterField(
model_name='payment',
name='status',
field=models.CharField(
choices=[
('pending_approval', 'Pending Approval'),
('succeeded', 'Succeeded'),
('failed', 'Failed'),
('refunded', 'Refunded')
],
db_index=True,
default='pending_approval',
max_length=20
),
),
]