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>
This commit is contained in:
254
backend/app/services/promo_notification_service.py
Normal file
254
backend/app/services/promo_notification_service.py
Normal file
@@ -0,0 +1,254 @@
|
||||
"""
|
||||
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))
|
||||
Reference in New Issue
Block a user