fix fix fi x fix
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user