fix fix fi x fix

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-12 15:30:15 +00:00
parent 7d4d309677
commit e8360a6703
11 changed files with 488 additions and 71 deletions

View File

@@ -1896,18 +1896,27 @@ class AutomationService:
"""
Get total credits used by this run so far.
Uses CreditUsageLog (same source as /account/usage/credits endpoint) for accuracy.
Filters by site to only count credits used by this specific automation run.
"""
if not self.run:
return 0
# FIXED: Use CreditUsageLog instead of counting AITaskLog records
# This matches the source of truth used by /account/usage/credits endpoint
# Use CreditUsageLog - the source of truth for credit usage
# Filter by site to get only credits used by this automation run
from igny8_core.business.billing.models import CreditUsageLog
from django.db.models import Sum
filters = {
'account': self.account,
'created_at__gte': self.run.started_at
}
# Filter by site if available for more accurate per-run counting
if self.site:
filters['site'] = self.site
total = CreditUsageLog.objects.filter(
account=self.account,
created_at__gte=self.run.started_at
**filters
).aggregate(total=Sum('credits_used'))['total'] or 0
return total

View File

@@ -1055,6 +1055,125 @@ Current Balance: {current_credits} credits
To avoid service interruption, please top up your credits:
{context['topup_url']}
Thank you,
The IGNY8 Team
""".strip(),
)
@staticmethod
def send_invoice_email(invoice, is_reminder=False):
"""
Send invoice email to the account owner.
Used for manual payment methods (bank transfer, local wallet, manual).
Args:
invoice: Invoice model instance
is_reminder: If True, sends as a reminder email
"""
service = get_email_service()
frontend_url = BillingEmailService._get_frontend_url()
account = invoice.account
subject_prefix = 'Reminder: ' if is_reminder else ''
context = {
'account_name': account.name,
'invoice_number': invoice.invoice_number or f'INV-{invoice.id}',
'invoice_date': invoice.created_at.strftime('%Y-%m-%d'),
'due_date': invoice.due_date.strftime('%Y-%m-%d') if invoice.due_date else 'N/A',
'total_amount': invoice.total_amount,
'currency': invoice.currency or 'USD',
'items': invoice.items or [],
'payment_instructions': invoice.metadata.get('payment_instructions', ''),
'is_reminder': is_reminder,
'frontend_url': frontend_url,
'invoice_url': f'{frontend_url}/billing/invoices/{invoice.id}',
}
subject = f'{subject_prefix}Invoice #{context["invoice_number"]} - Payment Required'
try:
result = service.send_transactional(
to=account.billing_email or account.owner.email,
subject=subject,
template='emails/invoice.html',
context=context,
tags=['billing', 'invoice', 'reminder' if is_reminder else 'new'],
)
logger.info(f'Invoice email sent for Invoice {invoice.id} (reminder={is_reminder})')
return result
except Exception as e:
logger.error(f'Failed to send invoice email: {str(e)}')
reminder_text = 'This is a reminder that your ' if is_reminder else ''
return service.send_transactional(
to=account.billing_email or account.owner.email,
subject=subject,
text=f"""
Hi {account.name},
{reminder_text}Invoice #{context['invoice_number']} requires your attention.
Invoice Details:
- Invoice Number: {context['invoice_number']}
- Date: {context['invoice_date']}
- Due Date: {context['due_date']}
- Amount: {context['currency']} {context['total_amount']}
To view and pay your invoice:
{context['invoice_url']}
{context['payment_instructions']}
If you have any questions, please contact our support team.
Thank you,
The IGNY8 Team
""".strip(),
)
@staticmethod
def send_subscription_expired_email(account, subscription):
"""
Send email when subscription expires due to non-payment.
"""
service = get_email_service()
frontend_url = BillingEmailService._get_frontend_url()
context = {
'account_name': account.name,
'plan_name': subscription.plan.name if subscription.plan else 'N/A',
'frontend_url': frontend_url,
'billing_url': f'{frontend_url}/account/plans',
}
try:
result = service.send_transactional(
to=account.billing_email or account.owner.email,
subject='Subscription Expired - Action Required',
template='emails/subscription_expired.html',
context=context,
tags=['billing', 'subscription-expired'],
)
logger.info(f'Subscription expired email sent for account {account.id}')
return result
except Exception as e:
logger.error(f'Failed to send subscription expired email: {str(e)}')
return service.send_transactional(
to=account.billing_email or account.owner.email,
subject='Subscription Expired - Action Required',
text=f"""
Hi {account.name},
Your subscription to the {context['plan_name']} plan has expired due to non-payment.
Your account access may be limited until you renew your subscription.
To reactivate your subscription:
{context['billing_url']}
If you have any questions or need assistance, please contact our support team.
Thank you,
The IGNY8 Team
""".strip(),

View File

@@ -14,6 +14,11 @@ import logging
logger = logging.getLogger(__name__)
# Grace period in days for manual payment before subscription expires
RENEWAL_GRACE_PERIOD_DAYS = 7
# Days between invoice reminder emails
INVOICE_REMINDER_INTERVAL_DAYS = 3
@shared_task(name='billing.send_renewal_notices')
def send_renewal_notices():
@@ -99,7 +104,7 @@ def renew_subscription(subscription_id: int):
# Attempt automatic payment if payment method on file
payment_attempted = False
# Check if account has saved payment method
# Check if account has saved payment method for automatic billing
if subscription.metadata.get('stripe_subscription_id'):
payment_attempted = _attempt_stripe_renewal(subscription, invoice)
elif subscription.metadata.get('paypal_subscription_id'):
@@ -110,15 +115,24 @@ def renew_subscription(subscription_id: int):
logger.info(f"Automatic payment initiated for subscription {subscription_id}")
else:
# No automatic payment - send invoice for manual payment
# This handles all payment methods: bank_transfer, local_wallet, manual
logger.info(f"Manual payment required for subscription {subscription_id}")
# Mark subscription as pending renewal
# Mark subscription as pending renewal with grace period
grace_period_end = timezone.now() + timedelta(days=RENEWAL_GRACE_PERIOD_DAYS)
subscription.status = 'pending_renewal'
subscription.metadata['renewal_invoice_id'] = invoice.id
subscription.metadata['renewal_required_at'] = timezone.now().isoformat()
subscription.metadata['grace_period_end'] = grace_period_end.isoformat()
subscription.metadata['last_invoice_reminder_at'] = timezone.now().isoformat()
subscription.save(update_fields=['status', 'metadata'])
# TODO: Send invoice email
# Send invoice email for manual payment
try:
BillingEmailService.send_invoice_email(invoice, is_reminder=False)
logger.info(f"Invoice email sent for subscription {subscription_id}")
except Exception as e:
logger.exception(f"Failed to send invoice email for subscription {subscription_id}: {str(e)}")
# Clear renewal notice flag
if 'renewal_notice_sent' in subscription.metadata:
@@ -131,6 +145,117 @@ def renew_subscription(subscription_id: int):
logger.exception(f"Error renewing subscription {subscription_id}: {str(e)}")
@shared_task(name='billing.send_invoice_reminders')
def send_invoice_reminders():
"""
Send invoice reminder emails for pending renewals
Run daily to remind accounts with pending invoices
"""
now = timezone.now()
reminder_threshold = now - timedelta(days=INVOICE_REMINDER_INTERVAL_DAYS)
# Get subscriptions pending renewal
subscriptions = Subscription.objects.filter(
status='pending_renewal'
).select_related('account', 'plan')
for subscription in subscriptions:
# Check if enough time has passed since last reminder
last_reminder = subscription.metadata.get('last_invoice_reminder_at')
if last_reminder:
from datetime import datetime
last_reminder_dt = datetime.fromisoformat(last_reminder.replace('Z', '+00:00'))
if hasattr(last_reminder_dt, 'tzinfo') and last_reminder_dt.tzinfo is None:
last_reminder_dt = timezone.make_aware(last_reminder_dt)
if last_reminder_dt > reminder_threshold:
continue
# Get the renewal invoice
invoice_id = subscription.metadata.get('renewal_invoice_id')
if not invoice_id:
continue
try:
invoice = Invoice.objects.get(id=invoice_id)
# Only send reminder for unpaid invoices
if invoice.status not in ['pending', 'overdue']:
continue
BillingEmailService.send_invoice_email(invoice, is_reminder=True)
# Update last reminder timestamp
subscription.metadata['last_invoice_reminder_at'] = now.isoformat()
subscription.save(update_fields=['metadata'])
logger.info(f"Invoice reminder sent for subscription {subscription.id}")
except Invoice.DoesNotExist:
logger.warning(f"Invoice {invoice_id} not found for subscription {subscription.id}")
except Exception as e:
logger.exception(f"Failed to send invoice reminder for subscription {subscription.id}: {str(e)}")
@shared_task(name='billing.check_expired_renewals')
def check_expired_renewals():
"""
Check for subscriptions that have exceeded their grace period
and automatically change their status to expired
Run daily
"""
now = timezone.now()
# Get subscriptions pending renewal
subscriptions = Subscription.objects.filter(
status='pending_renewal'
).select_related('account', 'plan')
expired_count = 0
for subscription in subscriptions:
grace_period_end = subscription.metadata.get('grace_period_end')
if not grace_period_end:
# No grace period set, use default from renewal_required_at
renewal_required_at = subscription.metadata.get('renewal_required_at')
if renewal_required_at:
from datetime import datetime
required_dt = datetime.fromisoformat(renewal_required_at.replace('Z', '+00:00'))
if hasattr(required_dt, 'tzinfo') and required_dt.tzinfo is None:
required_dt = timezone.make_aware(required_dt)
grace_period_end = (required_dt + timedelta(days=RENEWAL_GRACE_PERIOD_DAYS)).isoformat()
else:
continue
# Check if grace period has expired
from datetime import datetime
grace_end_dt = datetime.fromisoformat(grace_period_end.replace('Z', '+00:00'))
if hasattr(grace_end_dt, 'tzinfo') and grace_end_dt.tzinfo is None:
grace_end_dt = timezone.make_aware(grace_end_dt)
if now > grace_end_dt:
# Grace period expired - change status to expired
subscription.status = 'expired'
subscription.metadata['expired_at'] = now.isoformat()
subscription.metadata['expired_reason'] = 'Grace period exceeded without payment'
subscription.save(update_fields=['status', 'metadata'])
expired_count += 1
logger.info(f"Subscription {subscription.id} expired due to non-payment")
# Send expiration notification
try:
BillingEmailService.send_subscription_expired_email(
subscription.account,
subscription
)
except Exception as e:
logger.exception(f"Failed to send expiration email for subscription {subscription.id}: {str(e)}")
if expired_count > 0:
logger.info(f"Expired {expired_count} subscriptions due to non-payment")
def _attempt_stripe_renewal(subscription: Subscription, invoice: Invoice) -> bool:
"""
Attempt to charge Stripe subscription