Files
AutonetSellCar/backend/app/services/promo_notification_service.py
AutonetSellCar Deploy 2720689515 feat: Add promo preference survey on main page
- Add promo preference fields to User model (promo_preferred_maker,
  promo_preferred_model, promo_email_enabled)
- Create API endpoints for getting/updating promo preferences
- Create PromoPreference component with maker/model selection
- Show login prompt for non-logged-in users when interacting
- Add promo notification service to send emails when matching vehicles
  are added to promotion
- Add multi-language translations (en, mn, ru, ko)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 23:37:31 +09:00

255 lines
8.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Promo Notification Service
Sends email notifications to users when their preferred vehicle is added to a promotion
"""
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from typing import List, Optional
from sqlalchemy.orm import Session
import asyncio
from ..config import get_settings
from ..models.user import User
from ..models.car import Car
settings = get_settings()
def get_users_with_matching_preference(
db: Session,
maker_name: Optional[str] = None,
model_name: Optional[str] = None
) -> List[User]:
"""
Find users whose promo preference matches the given maker/model
Only returns users who have promo_email_enabled = True
"""
query = db.query(User).filter(
User.promo_email_enabled == True,
User.email.isnot(None)
)
if maker_name:
# Match maker only (for users who selected maker but any model)
# or match exact maker+model
query = query.filter(User.promo_preferred_maker == maker_name)
if model_name:
# If model is specified, also filter by model (or users with no model preference)
query = query.filter(
(User.promo_preferred_model == model_name) |
(User.promo_preferred_model.is_(None)) |
(User.promo_preferred_model == "")
)
return query.all()
async def send_promo_notification_email(
email: str,
car: Car,
language: str = "en"
) -> bool:
"""Send promo notification email to a single user"""
try:
# Email templates by language
subjects = {
"en": f"AutonetSellCar - Your Preferred Vehicle is Now on Promotion!",
"ko": f"AutonetSellCar - 관심 차량이 프로모션에 등록되었습니다!",
"mn": f"AutonetSellCar - Таны сонирхсон машин урамшуулалд орлоо!",
"ru": f"AutonetSellCar - Ваш желаемый автомобиль теперь в акции!"
}
car_name = car.car_name or "Unknown Vehicle"
car_year = car.year or ""
car_mileage = f"{car.mileage:,}km" if car.mileage else ""
car_url = f"https://autonetsellcar.com/cars/{car.id}"
bodies = {
"en": f"""
Hello,
Great news! A vehicle matching your preference is now available on promotion!
Vehicle: {car_name}
Year: {car_year}
Mileage: {car_mileage}
View this vehicle: {car_url}
As a promoted vehicle, you can view all photos for free!
Best regards,
AutonetSellCar Team
---
You received this email because you signed up for promotion notifications.
To unsubscribe, update your preferences in your account settings.
""",
"ko": f"""
안녕하세요,
좋은 소식입니다! 관심 차량이 프로모션에 등록되었습니다!
차량: {car_name}
연식: {car_year}
주행거리: {car_mileage}
차량 보기: {car_url}
프로모션 차량은 모든 사진을 무료로 보실 수 있습니다!
감사합니다,
AutonetSellCar 팀
---
프로모션 알림 신청으로 이 이메일을 받으셨습니다.
수신 거부를 원하시면 계정 설정에서 변경해 주세요.
""",
"mn": f"""
Сайн байна уу,
Сайхан мэдээ! Таны сонирхсон машин урамшуулалд орлоо!
Машин: {car_name}
Он: {car_year}
Гүйлт: {car_mileage}
Машин үзэх: {car_url}
Урамшуулалтай машины бүх зургийг үнэгүй үзэх боломжтой!
Хүндэтгэсэн,
AutonetSellCar баг
---
Та урамшуулалын мэдэгдэл хүлээн авахаар бүртгүүлсэн тул энэ имэйлийг хүлээн авлаа.
Татгалзахыг хүсвэл данс тохиргооноос өөрчилнө үү.
""",
"ru": f"""
Здравствуйте,
Отличные новости! Автомобиль, соответствующий вашим предпочтениям, теперь в акции!
Автомобиль: {car_name}
Год: {car_year}
Пробег: {car_mileage}
Посмотреть автомобиль: {car_url}
Как акционный автомобиль, вы можете просмотреть все фотографии бесплатно!
С уважением,
Команда AutonetSellCar
---
Вы получили это письмо, потому что подписались на уведомления об акциях.
Чтобы отписаться, измените настройки в вашем аккаунте.
"""
}
subject = subjects.get(language, subjects["en"])
body = bodies.get(language, bodies["en"])
# Check if SMTP is configured
if not settings.SMTP_USER or not settings.SMTP_PASSWORD:
# Development mode - just log
print(f"[DEV] Promo notification email for {email}: {car_name}")
return True
# Send actual email
msg = MIMEMultipart()
msg['From'] = f"{settings.SMTP_FROM_NAME} <{settings.SMTP_FROM_EMAIL or settings.SMTP_USER}>"
msg['To'] = email
msg['Subject'] = subject
msg.attach(MIMEText(body, 'plain', 'utf-8'))
with smtplib.SMTP(settings.SMTP_HOST, settings.SMTP_PORT) as server:
server.starttls()
server.login(settings.SMTP_USER, settings.SMTP_PASSWORD)
server.send_message(msg)
print(f"[INFO] Promo notification sent to {email} for car {car.id}")
return True
except Exception as e:
print(f"[ERROR] Failed to send promo notification to {email}: {e}")
return False
async def notify_users_for_promo_vehicle(
db: Session,
car: Car
) -> int:
"""
Notify all users whose preference matches the car being promoted
Returns the number of notifications sent
"""
# Get maker and model names from car
maker_name = car.maker.name if car.maker else None
model_name = car.model.name if car.model else None
if not maker_name:
print(f"[INFO] Car {car.id} has no maker, skipping promo notifications")
return 0
# Find matching users
matching_users = get_users_with_matching_preference(
db=db,
maker_name=maker_name,
model_name=model_name
)
if not matching_users:
print(f"[INFO] No users with matching preference for {maker_name} {model_name}")
return 0
print(f"[INFO] Found {len(matching_users)} users with preference for {maker_name} {model_name}")
# Send notifications
sent_count = 0
for user in matching_users:
if user.email:
# Determine language based on user's country
language = "en" # Default
if user.country:
country_lower = user.country.lower()
if country_lower in ["korea", "south korea", "kr", "한국"]:
language = "ko"
elif country_lower in ["mongolia", "mn", "монгол"]:
language = "mn"
elif country_lower in ["russia", "ru", "россия"]:
language = "ru"
success = await send_promo_notification_email(
email=user.email,
car=car,
language=language
)
if success:
sent_count += 1
return sent_count
def notify_users_for_promo_vehicle_sync(db: Session, car: Car) -> int:
"""Synchronous wrapper for notify_users_for_promo_vehicle"""
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
if loop.is_running():
# If already in async context, create a task
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor() as executor:
future = executor.submit(
asyncio.run,
notify_users_for_promo_vehicle(db, car)
)
return future.result()
else:
return loop.run_until_complete(notify_users_for_promo_vehicle(db, car))