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

@@ -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)),