STripe Paymen and PK payemtns and many othe rbacekd and froentened issues

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-07 05:51:36 +00:00
parent 87d1662a18
commit 0386d4bf33
24 changed files with 1079 additions and 174 deletions

View File

@@ -214,6 +214,7 @@ class AccountAdmin(ExportMixin, AccountAdminMixin, SimpleHistoryAdmin, Igny8Mode
'bulk_add_credits',
'bulk_subtract_credits',
'bulk_soft_delete',
'bulk_hard_delete',
]
def get_queryset(self, request):
@@ -454,14 +455,32 @@ class AccountAdmin(ExportMixin, AccountAdminMixin, SimpleHistoryAdmin, Igny8Mode
bulk_subtract_credits.short_description = 'Subtract credits from accounts'
def bulk_soft_delete(self, request, queryset):
"""Soft delete selected accounts"""
"""Soft delete selected accounts and all related data"""
count = 0
for account in queryset:
if account.slug != 'aws-admin': # Protect admin account
account.delete() # Soft delete via SoftDeletableModel
account.delete() # Soft delete via SoftDeletableModel (now cascades)
count += 1
self.message_user(request, f'{count} account(s) soft deleted.', messages.SUCCESS)
bulk_soft_delete.short_description = 'Soft delete selected accounts'
self.message_user(request, f'{count} account(s) and all related data soft deleted.', messages.SUCCESS)
bulk_soft_delete.short_description = 'Soft delete accounts (with cascade)'
def bulk_hard_delete(self, request, queryset):
"""PERMANENTLY delete selected accounts and ALL related data - cannot be undone!"""
count = 0
errors = []
for account in queryset:
if account.slug != 'aws-admin': # Protect admin account
try:
account.hard_delete_with_cascade() # Permanently delete everything
count += 1
except Exception as e:
errors.append(f'{account.name}: {str(e)}')
if count > 0:
self.message_user(request, f'{count} account(s) and ALL related data permanently deleted.', messages.SUCCESS)
if errors:
self.message_user(request, f'Errors: {"; ".join(errors)}', messages.ERROR)
bulk_hard_delete.short_description = '⚠️ PERMANENTLY delete accounts (irreversible!)'
class SubscriptionResource(resources.ModelResource):

View File

@@ -153,12 +153,144 @@ class Account(SoftDeletableModel):
# System accounts bypass all filtering restrictions
return self.slug in ['aws-admin', 'default-account', 'default']
def soft_delete(self, user=None, reason=None, retention_days=None):
def soft_delete(self, user=None, reason=None, retention_days=None, cascade=True):
"""
Soft delete the account and optionally cascade to all related objects.
Args:
user: User performing the deletion
reason: Reason for deletion
retention_days: Days before permanent deletion
cascade: If True, also soft-delete related objects that support soft delete,
and hard-delete objects that don't support soft delete
"""
if self.is_system_account():
from django.core.exceptions import PermissionDenied
raise PermissionDenied("System account cannot be deleted.")
if cascade:
self._cascade_delete_related(user=user, reason=reason, retention_days=retention_days, hard_delete=False)
return super().soft_delete(user=user, reason=reason, retention_days=retention_days)
def _cascade_delete_related(self, user=None, reason=None, retention_days=None, hard_delete=False):
"""
Delete all related objects when account is deleted.
For soft delete: soft-deletes objects with SoftDeletableModel, hard-deletes others
For hard delete: hard-deletes everything
"""
from igny8_core.common.soft_delete import SoftDeletableModel
# List of related objects to delete (in order to avoid FK constraint issues)
# Related names from Account reverse relations
related_names = [
# Content & Planning related (delete first due to dependencies)
'contentclustermap_set',
'contentattribute_set',
'contenttaxonomy_set',
'content_set',
'images_set',
'contentideas_set',
'tasks_set',
'keywords_set',
'clusters_set',
'strategy_set',
# Automation
'automation_runs',
'automation_configs',
# Publishing & Integration
'syncevent_set',
'publishingsettings_set',
'publishingrecord_set',
'deploymentrecord_set',
'siteintegration_set',
# Notifications & Optimization
'notification_set',
'optimizationtask_set',
# AI & Settings
'aitasklog_set',
'aiprompt_set',
'aisettings_set',
'authorprofile_set',
# Billing (preserve invoices/payments for audit, delete others)
'planlimitusage_set',
'creditusagelog_set',
'credittransaction_set',
'accountpaymentmethod_set',
'payment_set',
'invoice_set',
# Settings
'modulesettings_set',
'moduleenablesettings_set',
'integrationsettings_set',
'user_settings',
'accountsettings_set',
# Core (last due to dependencies)
'sector_set',
'site_set',
# Subscription (OneToOne)
'subscription',
]
for related_name in related_names:
try:
related = getattr(self, related_name, None)
if related is None:
continue
# Handle OneToOne fields (subscription)
if hasattr(related, 'pk'):
# It's a single object (OneToOneField)
if hard_delete:
related.hard_delete() if hasattr(related, 'hard_delete') else related.delete()
elif isinstance(related, SoftDeletableModel):
related.soft_delete(user=user, reason=reason, retention_days=retention_days)
else:
# Non-soft-deletable single object - hard delete
related.delete()
else:
# It's a RelatedManager (ForeignKey)
queryset = related.all()
if queryset.exists():
if hard_delete:
# Hard delete all
if hasattr(queryset, 'hard_delete'):
queryset.hard_delete()
else:
for obj in queryset:
if hasattr(obj, 'hard_delete'):
obj.hard_delete()
else:
obj.delete()
else:
# Soft delete if supported, otherwise hard delete
model = queryset.model
if issubclass(model, SoftDeletableModel):
for obj in queryset:
obj.soft_delete(user=user, reason=reason, retention_days=retention_days)
else:
queryset.delete()
except Exception as e:
# Log but don't fail - some relations may not exist
import logging
logger = logging.getLogger(__name__)
logger.warning(f"Failed to delete related {related_name} for account {self.pk}: {e}")
def hard_delete_with_cascade(self, using=None, keep_parents=False):
"""
Permanently delete the account and ALL related objects.
This bypasses soft-delete and removes everything from the database.
USE WITH CAUTION - this cannot be undone!
"""
if self.is_system_account():
from django.core.exceptions import PermissionDenied
raise PermissionDenied("System account cannot be deleted.")
# Cascade hard-delete all related objects first
self._cascade_delete_related(hard_delete=True)
# Finally hard-delete the account itself
return super().hard_delete(using=using, keep_parents=keep_parents)
def delete(self, using=None, keep_parents=False):
return self.soft_delete()

View File

@@ -406,11 +406,20 @@ class RegisterSerializer(serializers.Serializer):
)
# Generate unique slug for account
base_slug = account_name.lower().replace(' ', '-').replace('_', '-')[:50] or 'account'
slug = base_slug
# Clean the base slug: lowercase, replace spaces and underscores with hyphens
import re
import random
import string
base_slug = re.sub(r'[^a-z0-9-]', '', account_name.lower().replace(' ', '-').replace('_', '-'))[:40] or 'account'
# Add random suffix to prevent collisions (especially during concurrent registrations)
random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
slug = f"{base_slug}-{random_suffix}"
# Ensure uniqueness with fallback counter
counter = 1
while Account.objects.filter(slug=slug).exists():
slug = f"{base_slug}-{counter}"
slug = f"{base_slug}-{random_suffix}-{counter}"
counter += 1
# Create account with status and credits seeded (0 for paid pending)

View File

@@ -109,16 +109,70 @@ class RegisterView(APIView):
refresh_expires_at = timezone.now() + get_refresh_token_expiry()
user_serializer = UserSerializer(user)
# Build response data
response_data = {
'user': user_serializer.data,
'tokens': {
'access': access_token,
'refresh': refresh_token,
'access_expires_at': access_expires_at.isoformat(),
'refresh_expires_at': refresh_expires_at.isoformat(),
}
}
# For Stripe payment method with paid plan, create checkout session
payment_method = request.data.get('payment_method', '')
import logging
logger = logging.getLogger(__name__)
logger.info(f"Registration: payment_method={payment_method}, account_status={account.status if account else 'no account'}")
if account and account.status == 'pending_payment' and payment_method == 'stripe':
try:
from igny8_core.business.billing.services.stripe_service import StripeService
stripe_service = StripeService()
logger.info(f"Creating Stripe checkout for account {account.id}, plan {account.plan.name}")
checkout_data = stripe_service.create_checkout_session(
account=account,
plan=account.plan,
)
logger.info(f"Stripe checkout created: {checkout_data}")
response_data['checkout_url'] = checkout_data.get('checkout_url')
response_data['checkout_session_id'] = checkout_data.get('session_id')
except Exception as e:
logger.error(f"Failed to create Stripe checkout session: {e}", exc_info=True)
# Don't fail registration, just log the error
# User can still complete payment from the plans page
# For PayPal payment method with paid plan, create PayPal order
elif account and account.status == 'pending_payment' and payment_method == 'paypal':
try:
from django.conf import settings
from igny8_core.business.billing.services.paypal_service import PayPalService
paypal_service = PayPalService()
frontend_url = getattr(settings, 'FRONTEND_URL', 'https://app.igny8.com')
logger.info(f"Creating PayPal order for account {account.id}, amount {account.plan.price}")
order = paypal_service.create_order(
account=account,
amount=float(account.plan.price),
description=f'{account.plan.name} Plan Subscription',
return_url=f'{frontend_url}/account/plans?paypal=success&plan_id={account.plan.id}',
cancel_url=f'{frontend_url}/account/plans?paypal=cancel',
metadata={
'plan_id': str(account.plan.id),
'type': 'subscription',
}
)
logger.info(f"PayPal order created: {order}")
response_data['checkout_url'] = order.get('approval_url')
response_data['paypal_order_id'] = order.get('order_id')
except Exception as e:
logger.error(f"Failed to create PayPal order: {e}", exc_info=True)
# Don't fail registration, just log the error
return success_response(
data={
'user': user_serializer.data,
'tokens': {
'access': access_token,
'refresh': refresh_token,
'access_expires_at': access_expires_at.isoformat(),
'refresh_expires_at': refresh_expires_at.isoformat(),
}
},
data=response_data,
message='Registration successful',
status_code=status.HTTP_201_CREATED,
request=request