Update.
This commit is contained in:
parent
1df5621c90
commit
cf800df2a9
@ -133,6 +133,50 @@ class InvoiceGenerator:
|
||||
except Exception as e:
|
||||
print(f"Failed to finalize Stripe invoice: {e}")
|
||||
|
||||
# Send invoice email
|
||||
from ..mail import queue_email
|
||||
line_items = await invoice.line_items.all()
|
||||
items_text = "\n".join([f"- {item.description}: ${item.amount}" for item in line_items])
|
||||
body = f"""Dear {invoice.user.username},
|
||||
|
||||
Your invoice {invoice.invoice_number} for the period {invoice.period_start} to {invoice.period_end} is now available.
|
||||
|
||||
Invoice Details:
|
||||
{items_text}
|
||||
|
||||
Subtotal: ${invoice.subtotal}
|
||||
Tax: ${invoice.tax}
|
||||
Total: ${invoice.total}
|
||||
|
||||
Due Date: {invoice.due_date}
|
||||
|
||||
You can view and pay your invoice at: {invoice.user.email} # Placeholder, should be a link to invoice page
|
||||
|
||||
Best regards,
|
||||
The RBox Team
|
||||
"""
|
||||
html = f"""
|
||||
<h2>Invoice {invoice.invoice_number}</h2>
|
||||
<p>Dear {invoice.user.username},</p>
|
||||
<p>Your invoice for the period {invoice.period_start} to {invoice.period_end} is now available.</p>
|
||||
<table border="1">
|
||||
<tr><th>Description</th><th>Amount</th></tr>
|
||||
{"".join([f"<tr><td>{item.description}</td><td>${item.amount}</td></tr>" for item in line_items])}
|
||||
<tr><td><strong>Subtotal</strong></td><td><strong>${invoice.subtotal}</strong></td></tr>
|
||||
<tr><td><strong>Tax</strong></td><td><strong>${invoice.tax}</strong></td></tr>
|
||||
<tr><td><strong>Total</strong></td><td><strong>${invoice.total}</strong></td></tr>
|
||||
</table>
|
||||
<p>Due Date: {invoice.due_date}</p>
|
||||
<p>You can view and pay your invoice at: <a href="#">Invoice Link</a></p>
|
||||
<p>Best regards,<br>The RBox Team</p>
|
||||
"""
|
||||
queue_email(
|
||||
to_email=invoice.user.email,
|
||||
subject=f"Your RBox Invoice {invoice.invoice_number}",
|
||||
body=body,
|
||||
html=html
|
||||
)
|
||||
|
||||
return invoice
|
||||
|
||||
@staticmethod
|
||||
|
||||
105
rbox/mail.py
Normal file
105
rbox/mail.py
Normal file
@ -0,0 +1,105 @@
|
||||
import asyncio
|
||||
import aiosmtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from typing import Optional, Dict, Any
|
||||
from .settings import settings
|
||||
|
||||
class EmailTask:
|
||||
def __init__(self, to_email: str, subject: str, body: str, html: Optional[str] = None, **kwargs):
|
||||
self.to_email = to_email
|
||||
self.subject = subject
|
||||
self.body = body
|
||||
self.html = html
|
||||
self.kwargs = kwargs
|
||||
|
||||
class EmailService:
|
||||
def __init__(self):
|
||||
self.queue = asyncio.Queue()
|
||||
self.worker_task: Optional[asyncio.Task] = None
|
||||
self.running = False
|
||||
|
||||
async def start(self):
|
||||
"""Start the email worker"""
|
||||
if self.running:
|
||||
return
|
||||
self.running = True
|
||||
self.worker_task = asyncio.create_task(self._worker())
|
||||
|
||||
async def stop(self):
|
||||
"""Stop the email worker"""
|
||||
if not self.running:
|
||||
return
|
||||
self.running = False
|
||||
if self.worker_task:
|
||||
self.worker_task.cancel()
|
||||
try:
|
||||
await self.worker_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
async def send_email(self, to_email: str, subject: str, body: str, html: Optional[str] = None, **kwargs):
|
||||
"""Queue an email for sending"""
|
||||
task = EmailTask(to_email, subject, body, html, **kwargs)
|
||||
await self.queue.put(task)
|
||||
|
||||
async def _worker(self):
|
||||
"""Email worker coroutine"""
|
||||
while self.running:
|
||||
try:
|
||||
# Wait for a task with timeout to allow checking running flag
|
||||
task = await asyncio.wait_for(self.queue.get(), timeout=1.0)
|
||||
await self._send_email_task(task)
|
||||
self.queue.task_done()
|
||||
except asyncio.TimeoutError:
|
||||
continue
|
||||
except Exception as e:
|
||||
# Log error, but continue processing
|
||||
print(f"Email worker error: {e}")
|
||||
continue
|
||||
|
||||
async def _send_email_task(self, task: EmailTask):
|
||||
"""Send a single email task"""
|
||||
if not settings.SMTP_SERVER or not settings.SMTP_USERNAME or not settings.SMTP_PASSWORD:
|
||||
print("SMTP not configured, skipping email send")
|
||||
return
|
||||
|
||||
msg = MIMEMultipart('alternative')
|
||||
msg['From'] = settings.SMTP_FROM_EMAIL
|
||||
msg['To'] = task.to_email
|
||||
msg['Subject'] = task.subject
|
||||
|
||||
# Add text part
|
||||
text_part = MIMEText(task.body, 'plain')
|
||||
msg.attach(text_part)
|
||||
|
||||
# Add HTML part if provided
|
||||
if task.html:
|
||||
html_part = MIMEText(task.html, 'html')
|
||||
msg.attach(html_part)
|
||||
|
||||
try:
|
||||
async with aiosmtplib.SMTP(
|
||||
hostname=settings.SMTP_SERVER,
|
||||
port=settings.SMTP_PORT,
|
||||
username=settings.SMTP_USERNAME,
|
||||
password=settings.SMTP_PASSWORD,
|
||||
use_tls=True
|
||||
) as smtp:
|
||||
await smtp.send_message(msg)
|
||||
print(f"Email sent to {task.to_email}")
|
||||
except Exception as e:
|
||||
print(f"Failed to send email to {task.to_email}: {e}")
|
||||
raise # Re-raise to let caller handle
|
||||
|
||||
# Global email service instance
|
||||
email_service = EmailService()
|
||||
|
||||
# Convenience functions
|
||||
async def send_email(to_email: str, subject: str, body: str, html: Optional[str] = None, **kwargs):
|
||||
"""Send an email asynchronously"""
|
||||
await email_service.send_email(to_email, subject, body, html, **kwargs)
|
||||
|
||||
def queue_email(to_email: str, subject: str, body: str, html: Optional[str] = None, **kwargs):
|
||||
"""Queue an email for sending (fire and forget)"""
|
||||
asyncio.create_task(email_service.send_email(to_email, subject, body, html, **kwargs))
|
||||
@ -20,8 +20,11 @@ async def lifespan(app: FastAPI):
|
||||
logger.info("Database connected.")
|
||||
from .billing.scheduler import start_scheduler
|
||||
from .billing.models import PricingConfig
|
||||
from .mail import email_service
|
||||
start_scheduler()
|
||||
logger.info("Billing scheduler started")
|
||||
await email_service.start()
|
||||
logger.info("Email service started")
|
||||
pricing_count = await PricingConfig.all().count()
|
||||
if pricing_count == 0:
|
||||
from decimal import Decimal
|
||||
@ -38,6 +41,8 @@ async def lifespan(app: FastAPI):
|
||||
from .billing.scheduler import stop_scheduler
|
||||
stop_scheduler()
|
||||
logger.info("Billing scheduler stopped")
|
||||
await email_service.stop()
|
||||
logger.info("Email service stopped")
|
||||
print("Shutting down...")
|
||||
|
||||
app = FastAPI(
|
||||
|
||||
@ -96,3 +96,14 @@ async def delete_user_by_admin(user_id: int):
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||
await user.delete()
|
||||
return {"message": "User deleted successfully"}
|
||||
|
||||
@router.post("/test-email")
|
||||
async def send_test_email(to_email: str, subject: str = "Test Email", body: str = "This is a test email"):
|
||||
from ..mail import queue_email
|
||||
queue_email(
|
||||
to_email=to_email,
|
||||
subject=subject,
|
||||
body=body,
|
||||
html=f"<h1>{subject}</h1><p>{body}</p>"
|
||||
)
|
||||
return {"message": "Test email queued"}
|
||||
@ -61,6 +61,15 @@ async def register_user(user_in: UserCreate):
|
||||
hashed_password=hashed_password,
|
||||
)
|
||||
|
||||
# Send welcome email
|
||||
from ..mail import queue_email
|
||||
queue_email(
|
||||
to_email=user.email,
|
||||
subject="Welcome to RBox!",
|
||||
body=f"Hi {user.username},\n\nWelcome to RBox! Your account has been created successfully.\n\nBest regards,\nThe RBox Team",
|
||||
html=f"<h1>Welcome to RBox!</h1><p>Hi {user.username},</p><p>Welcome to RBox! Your account has been created successfully.</p><p>Best regards,<br>The RBox Team</p>"
|
||||
)
|
||||
|
||||
access_token_expires = timedelta(minutes=30) # Use settings
|
||||
access_token = create_access_token(
|
||||
data={"sub": user.username}, expires_delta=access_token_expires
|
||||
|
||||
@ -118,3 +118,4 @@ watchfiles==1.1.1
|
||||
websockets==15.0.1
|
||||
yarl==1.22.0
|
||||
zstandard==0.25.0
|
||||
aiosmtplib==5.0.0
|
||||
|
||||
Loading…
Reference in New Issue
Block a user