Update.
This commit is contained in:
parent
1df5621c90
commit
cf800df2a9
@ -133,6 +133,50 @@ class InvoiceGenerator:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed to finalize Stripe invoice: {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
|
return invoice
|
||||||
|
|
||||||
@staticmethod
|
@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.")
|
logger.info("Database connected.")
|
||||||
from .billing.scheduler import start_scheduler
|
from .billing.scheduler import start_scheduler
|
||||||
from .billing.models import PricingConfig
|
from .billing.models import PricingConfig
|
||||||
|
from .mail import email_service
|
||||||
start_scheduler()
|
start_scheduler()
|
||||||
logger.info("Billing scheduler started")
|
logger.info("Billing scheduler started")
|
||||||
|
await email_service.start()
|
||||||
|
logger.info("Email service started")
|
||||||
pricing_count = await PricingConfig.all().count()
|
pricing_count = await PricingConfig.all().count()
|
||||||
if pricing_count == 0:
|
if pricing_count == 0:
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
@ -38,6 +41,8 @@ async def lifespan(app: FastAPI):
|
|||||||
from .billing.scheduler import stop_scheduler
|
from .billing.scheduler import stop_scheduler
|
||||||
stop_scheduler()
|
stop_scheduler()
|
||||||
logger.info("Billing scheduler stopped")
|
logger.info("Billing scheduler stopped")
|
||||||
|
await email_service.stop()
|
||||||
|
logger.info("Email service stopped")
|
||||||
print("Shutting down...")
|
print("Shutting down...")
|
||||||
|
|
||||||
app = FastAPI(
|
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")
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||||
await user.delete()
|
await user.delete()
|
||||||
return {"message": "User deleted successfully"}
|
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,
|
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_expires = timedelta(minutes=30) # Use settings
|
||||||
access_token = create_access_token(
|
access_token = create_access_token(
|
||||||
data={"sub": user.username}, expires_delta=access_token_expires
|
data={"sub": user.username}, expires_delta=access_token_expires
|
||||||
|
|||||||
@ -118,3 +118,4 @@ watchfiles==1.1.1
|
|||||||
websockets==15.0.1
|
websockets==15.0.1
|
||||||
yarl==1.22.0
|
yarl==1.22.0
|
||||||
zstandard==0.25.0
|
zstandard==0.25.0
|
||||||
|
aiosmtplib==5.0.0
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user