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:
AutonetSellCar Deploy
2026-01-12 23:37:31 +09:00
parent 2378392f95
commit 2720689515
10 changed files with 618 additions and 11 deletions

View File

@@ -10,6 +10,7 @@ from ..database import get_db
from ..models import User, SystemSettings, ReferralReward
from ..models.user import generate_referral_code
from ..schemas import UserCreate, UserUpdate, UserResponse, Token
from ..schemas.user import PromoPreferenceUpdate, PromoPreferenceResponse
from ..config import get_settings
router = APIRouter(prefix="/auth", tags=["auth"])
@@ -277,6 +278,28 @@ def update_me(
return current_user
@router.get("/me/promo-preference", response_model=PromoPreferenceResponse)
def get_promo_preference(current_user: User = Depends(get_current_user)):
"""프로모션 선호 차종 조회"""
return current_user
@router.put("/me/promo-preference", response_model=PromoPreferenceResponse)
def update_promo_preference(
promo_update: PromoPreferenceUpdate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""프로모션 선호 차종 설정"""
current_user.promo_preferred_maker = promo_update.promo_preferred_maker
current_user.promo_preferred_model = promo_update.promo_preferred_model
current_user.promo_email_enabled = promo_update.promo_email_enabled
db.commit()
db.refresh(current_user)
return current_user
# Admin User Management Endpoints
@router.get("/admin/users")
def admin_get_users(

View File

@@ -1,4 +1,4 @@
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query, Body
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query, Body, BackgroundTasks
from sqlalchemy.orm import Session
from typing import List, Optional
import os
@@ -17,6 +17,7 @@ from ..schemas.hero_banner import (
from .auth import get_current_user
from ..models import User
from ..config import get_settings
from ..services.promo_notification_service import notify_users_for_promo_vehicle
router = APIRouter(prefix="/hero-banners", tags=["hero-banners"])
@@ -309,15 +310,16 @@ async def upload_banner_image(
# ==================== Banner Toggle & Ordering ====================
@router.post("/admin/toggle/{car_id}")
def toggle_banner(
async def toggle_banner(
car_id: int,
background_tasks: BackgroundTasks,
db: Session = Depends(get_db),
current_user: User = Depends(get_admin_user)
):
"""차량의 배너 상태 토글 (Admin)
- HeroBanner 존재 → 삭제
- HeroBanner 없음 → 생성
- HeroBanner 없음 → 생성 (+ 프로모션 알림 이메일 전송)
"""
car = db.query(Car).filter(Car.id == car_id).first()
if not car:
@@ -360,4 +362,8 @@ def toggle_banner(
car.is_banner = True
db.commit()
db.refresh(banner)
# Send promo notification emails in background
background_tasks.add_task(notify_users_for_promo_vehicle, db, car)
return {"car_id": car_id, "is_banner": True, "banner_id": banner.id, "message": "Added to banner"}

View File

@@ -37,6 +37,11 @@ class User(Base):
phone_verified = Column(Boolean, default=False)
phone_verified_at = Column(DateTime(timezone=True), nullable=True)
# Promotion preference (프로모션 선호 차종)
promo_preferred_maker = Column(String(100), nullable=True) # 선호 메이커명
promo_preferred_model = Column(String(100), nullable=True) # 선호 모델명
promo_email_enabled = Column(Boolean, default=False) # 프로모션 이메일 수신 동의
# Account withdrawal/deletion
withdrawal_requested_at = Column(DateTime(timezone=True), nullable=True) # 탈퇴 요청 시각
withdrawal_reason = Column(String(500), nullable=True) # 탈퇴 사유

View File

@@ -42,12 +42,31 @@ class UserResponse(BaseModel):
referral_code: Optional[str] = None # User's unique referral code
email_verified: bool = False
phone_verified: bool = False
promo_preferred_maker: Optional[str] = None
promo_preferred_model: Optional[str] = None
promo_email_enabled: bool = False
created_at: datetime
class Config:
from_attributes = True
class PromoPreferenceUpdate(BaseModel):
"""Schema for updating promo preference"""
promo_preferred_maker: Optional[str] = None
promo_preferred_model: Optional[str] = None
promo_email_enabled: bool = False
class PromoPreferenceResponse(BaseModel):
promo_preferred_maker: Optional[str] = None
promo_preferred_model: Optional[str] = None
promo_email_enabled: bool = False
class Config:
from_attributes = True
class CarViewResponse(BaseModel):
id: int
user_id: int

View 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))

View File

@@ -3,6 +3,7 @@
import { useState, useEffect } from 'react';
import Link from 'next/link';
import FilmStripSlider from '@/components/FilmStripSlider';
import PromoPreference from '@/components/PromoPreference';
import { HeroBanner, HeroBannerSettings } from '@/types';
import { heroBannersApi } from '@/lib/api';
import { useTranslation } from '@/lib/i18n';
@@ -45,13 +46,23 @@ export default function Home() {
{/* Film Strip Slider */}
<FilmStripSlider banners={banners} settings={bannerSettings} />
<div className="container mx-auto px-4 py-4 sm:py-8 text-center">
<Link
href="/vehicle-request"
className="inline-block bg-yellow-500 text-white font-semibold px-6 py-2.5 sm:px-8 sm:py-3 rounded-lg hover:bg-yellow-600 transition shadow-lg text-sm sm:text-base"
>
{t.requestVehicle}
</Link>
<div className="container mx-auto px-4 py-4 sm:py-8">
<div className="flex flex-col lg:flex-row items-center justify-center gap-4 lg:gap-8">
{/* Request Vehicle Button */}
<Link
href="/vehicle-request"
className="inline-block bg-yellow-500 text-white font-semibold px-6 py-2.5 sm:px-8 sm:py-3 rounded-lg hover:bg-yellow-600 transition shadow-lg text-sm sm:text-base"
>
{t.requestVehicle}
</Link>
{/* Divider */}
<div className="hidden lg:block w-px h-24 bg-white/30"></div>
<div className="lg:hidden w-32 h-px bg-white/30"></div>
{/* Promo Preference */}
<PromoPreference />
</div>
</div>
</section>

View File

@@ -0,0 +1,213 @@
'use client';
import { useState, useEffect } from 'react';
import Link from 'next/link';
import { carsApi, authApi } from '@/lib/api';
import { useAuthStore } from '@/lib/store';
import { useTranslation } from '@/lib/i18n';
import { CarMaker, CarModel } from '@/types';
export default function PromoPreference() {
const { t } = useTranslation();
const { user, token } = useAuthStore();
const isLoggedIn = !!token && !!user;
const [makers, setMakers] = useState<CarMaker[]>([]);
const [models, setModels] = useState<CarModel[]>([]);
const [selectedMaker, setSelectedMaker] = useState<string>('');
const [selectedModel, setSelectedModel] = useState<string>('');
const [emailEnabled, setEmailEnabled] = useState(false);
const [loading, setLoading] = useState(false);
const [saved, setSaved] = useState(false);
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
// Load makers on mount
useEffect(() => {
const loadMakers = async () => {
try {
const data = await carsApi.getMakers();
setMakers(data);
} catch (error) {
console.error('Failed to load makers:', error);
}
};
loadMakers();
}, []);
// Load user preferences if logged in
useEffect(() => {
const loadPreferences = async () => {
if (!isLoggedIn) return;
try {
const pref = await authApi.getPromoPreference();
if (pref.promo_preferred_maker) {
setSelectedMaker(pref.promo_preferred_maker);
// Load models for selected maker
const maker = makers.find(m => m.name === pref.promo_preferred_maker);
if (maker) {
const modelsData = await carsApi.getModels(maker.id);
setModels(modelsData);
}
}
if (pref.promo_preferred_model) {
setSelectedModel(pref.promo_preferred_model);
}
setEmailEnabled(pref.promo_email_enabled);
} catch (error) {
console.error('Failed to load preferences:', error);
}
};
if (makers.length > 0) {
loadPreferences();
}
}, [isLoggedIn, makers]);
// Load models when maker changes
const handleMakerChange = async (makerName: string) => {
if (!isLoggedIn) {
setShowLoginPrompt(true);
return;
}
setShowLoginPrompt(false);
setSelectedMaker(makerName);
setSelectedModel('');
setSaved(false);
if (makerName) {
const maker = makers.find(m => m.name === makerName);
if (maker) {
try {
const data = await carsApi.getModels(maker.id);
setModels(data);
} catch (error) {
console.error('Failed to load models:', error);
}
}
} else {
setModels([]);
}
};
const handleModelChange = (modelName: string) => {
if (!isLoggedIn) {
setShowLoginPrompt(true);
return;
}
setShowLoginPrompt(false);
setSelectedModel(modelName);
setSaved(false);
};
const handleEmailToggle = () => {
if (!isLoggedIn) {
setShowLoginPrompt(true);
return;
}
setShowLoginPrompt(false);
setEmailEnabled(!emailEnabled);
setSaved(false);
};
const handleSave = async () => {
if (!isLoggedIn) {
setShowLoginPrompt(true);
return;
}
setLoading(true);
try {
await authApi.updatePromoPreference({
promo_preferred_maker: selectedMaker || undefined,
promo_preferred_model: selectedModel || undefined,
promo_email_enabled: emailEnabled,
});
setSaved(true);
setTimeout(() => setSaved(false), 3000);
} catch (error) {
console.error('Failed to save preferences:', error);
} finally {
setLoading(false);
}
};
return (
<div className="bg-white/10 backdrop-blur-sm rounded-lg p-4 sm:p-5 max-w-md mx-auto">
<h3 className="text-base sm:text-lg font-semibold text-white mb-3 text-center">
{t.promoPreferenceTitle || 'Next Promotion Interest'}
</h3>
<p className="text-xs sm:text-sm text-primary-100 mb-4 text-center">
{t.promoPreferenceDesc || 'Tell us which vehicle you want in the next promotion!'}
</p>
{/* Maker - Model Combo */}
<div className="space-y-3">
<div className="flex flex-col sm:flex-row gap-2">
<select
value={selectedMaker}
onChange={(e) => handleMakerChange(e.target.value)}
className="flex-1 px-3 py-2 rounded-lg text-sm text-gray-800 bg-white border-0 focus:ring-2 focus:ring-yellow-500"
>
<option value="">{t.selectMaker || 'Select Maker'}</option>
{makers.map((maker) => (
<option key={maker.id} value={maker.name}>
{maker.name}
</option>
))}
</select>
<select
value={selectedModel}
onChange={(e) => handleModelChange(e.target.value)}
disabled={!selectedMaker}
className="flex-1 px-3 py-2 rounded-lg text-sm text-gray-800 bg-white border-0 focus:ring-2 focus:ring-yellow-500 disabled:bg-gray-100 disabled:text-gray-400"
>
<option value="">{t.selectModel || 'Select Model'}</option>
{models.map((model) => (
<option key={model.id} value={model.name}>
{model.name}
</option>
))}
</select>
</div>
{/* Email Alert Checkbox */}
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={emailEnabled}
onChange={handleEmailToggle}
className="w-4 h-4 text-yellow-500 bg-white rounded border-0 focus:ring-yellow-500"
/>
<span className="text-xs sm:text-sm text-white">
{t.promoEmailAlert || 'Notify me by email when this vehicle is on promotion'}
</span>
</label>
{/* Login Prompt */}
{showLoginPrompt && (
<div className="p-3 bg-yellow-500/20 rounded-lg text-center">
<p className="text-sm text-white mb-2">
{t.promoLoginRequired || 'Please login to save your preference'}
</p>
<Link
href="/login"
className="inline-block text-sm font-medium text-yellow-400 hover:text-yellow-300 underline"
>
{t.login || 'Login'} / {t.signUp || 'Sign Up'}
</Link>
</div>
)}
{/* Save Button */}
{isLoggedIn && (selectedMaker || emailEnabled) && (
<button
onClick={handleSave}
disabled={loading}
className="w-full py-2 bg-yellow-500 text-white font-medium rounded-lg hover:bg-yellow-600 transition disabled:opacity-50 text-sm"
>
{loading ? (t.saving || 'Saving...') : saved ? (t.saved || 'Saved!') : (t.savePreference || 'Save Preference')}
</button>
)}
</div>
</div>
);
}

View File

@@ -1,5 +1,5 @@
import axios from 'axios';
import { Car, CarListResponse, CarMaker, CarModel, User, HeroBanner, HeroBannerSettings, CarView } from '@/types';
import { Car, CarListResponse, CarMaker, CarModel, User, HeroBanner, HeroBannerSettings, CarView, PromoPreference } from '@/types';
// When NEXT_PUBLIC_API_URL is empty, use relative path (for HTTPS proxy setup)
const API_URL = process.env.NEXT_PUBLIC_API_URL;
@@ -133,6 +133,18 @@ export const authApi = {
const { data } = await api.post('/auth/withdraw/cancel');
return data;
},
// 프로모션 선호 차종 조회
getPromoPreference: async (): Promise<PromoPreference> => {
const { data } = await api.get('/auth/me/promo-preference');
return data;
},
// 프로모션 선호 차종 설정
updatePromoPreference: async (preference: PromoPreference): Promise<PromoPreference> => {
const { data } = await api.put('/auth/me/promo-preference', preference);
return data;
},
};
// Inquiries API

View File

@@ -121,6 +121,17 @@ export interface Translations {
contactForPrice: string;
engine: string;
// Promo Preference
promoPreferenceTitle: string;
promoPreferenceDesc: string;
selectMaker: string;
selectModel: string;
promoEmailAlert: string;
promoLoginRequired: string;
savePreference: string;
saving: string;
saved: string;
// Vehicle Request & CC System
requestVehicle: string;
aiDealerSearching: string;
@@ -592,6 +603,17 @@ const translations: Record<Language, Translations> = {
contactForPrice: 'Contact for price',
engine: 'Engine',
// Promo Preference
promoPreferenceTitle: 'Next Promotion Interest',
promoPreferenceDesc: 'Tell us which vehicle you want in the next promotion!',
selectMaker: 'Select Maker',
selectModel: 'Select Model',
promoEmailAlert: 'Notify me by email when this vehicle is on promotion',
promoLoginRequired: 'Please login to save your preference',
savePreference: 'Save Preference',
saving: 'Saving...',
saved: 'Saved!',
// Vehicle Request & CC System
requestVehicle: 'Request Vehicle',
aiDealerSearching: 'Korean AI Dealer is searching for vehicles...',
@@ -1061,6 +1083,17 @@ const translations: Record<Language, Translations> = {
contactForPrice: 'Үнийг лавлана уу',
engine: 'Хөдөлгүүр',
// Promo Preference
promoPreferenceTitle: 'Дараагийн урамшуулалд сонирхолтой',
promoPreferenceDesc: 'Дараагийн урамшуулалд ямар машин хүсч байгаагаа хэлнэ үү!',
selectMaker: 'Үйлдвэрлэгч сонгох',
selectModel: 'Загвар сонгох',
promoEmailAlert: 'Энэ машин урамшуулалд гарахад имэйлээр мэдэгдэнэ үү',
promoLoginRequired: 'Сонголтоо хадгалахын тулд нэвтэрнэ үү',
savePreference: 'Сонголт хадгалах',
saving: 'Хадгалж байна...',
saved: 'Хадгалагдлаа!',
// Vehicle Request & CC System
requestVehicle: 'Машин захиалах',
aiDealerSearching: 'Солонгосын AI дилер машин хайж байна...',
@@ -1530,6 +1563,17 @@ const translations: Record<Language, Translations> = {
contactForPrice: 'Цена по запросу',
engine: 'Двигатель',
// Promo Preference
promoPreferenceTitle: 'Интерес к следующей акции',
promoPreferenceDesc: 'Расскажите, какой автомобиль вы хотите в следующей акции!',
selectMaker: 'Выберите марку',
selectModel: 'Выберите модель',
promoEmailAlert: 'Уведомить по email когда этот автомобиль появится в акции',
promoLoginRequired: 'Войдите, чтобы сохранить предпочтения',
savePreference: 'Сохранить предпочтения',
saving: 'Сохранение...',
saved: 'Сохранено!',
// Vehicle Request & CC System
requestVehicle: 'Запросить автомобиль',
aiDealerSearching: 'Корейский AI-дилер ищет автомобили...',
@@ -1999,6 +2043,17 @@ const translations: Record<Language, Translations> = {
contactForPrice: '가격 문의',
engine: '엔진',
// Promo Preference
promoPreferenceTitle: '다음 프로모션 관심 차종',
promoPreferenceDesc: '다음 프로모션에 원하는 차량을 알려주세요!',
selectMaker: '제조사 선택',
selectModel: '모델 선택',
promoEmailAlert: '이 차량이 프로모션에 등록되면 이메일로 알림받기',
promoLoginRequired: '선호도를 저장하려면 로그인하세요',
savePreference: '선호도 저장',
saving: '저장 중...',
saved: '저장됨!',
// Vehicle Request & CC System
requestVehicle: '차량요청하기',
aiDealerSearching: '한국AI딜러가 차량을 찾고있습니다...',

View File

@@ -106,9 +106,18 @@ export interface User {
referral_code?: string;
email_verified: boolean;
phone_verified: boolean;
promo_preferred_maker?: string;
promo_preferred_model?: string;
promo_email_enabled?: boolean;
created_at: string;
}
export interface PromoPreference {
promo_preferred_maker?: string;
promo_preferred_model?: string;
promo_email_enabled: boolean;
}
export interface CarView {
id: number;
user_id: number;