auto-commit for 8a62051e-b038-4363-a62d-7047e3d6e102

This commit is contained in:
emergent-agent-e1
2026-03-16 18:22:14 +00:00
parent 706e3e2eb6
commit 6a9e343332
16 changed files with 1510 additions and 212 deletions
+57
View File
@@ -0,0 +1,57 @@
from datetime import datetime, timedelta
from typing import Optional
from jose import JWTError, jwt
from passlib.context import CryptContext
from fastapi import HTTPException, Security, Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import os
# JWT Configuration
SECRET_KEY = os.environ.get("JWT_SECRET_KEY", "your-secret-key-change-in-production-epic-travel-2025")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 1440 # 24 hours
# Password hashing
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# Security
security = HTTPBearer()
def hash_password(password: str) -> str:
"""Hash a password"""
return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify a password against a hash"""
return pwd_context.verify(plain_password, hashed_password)
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
"""Create a JWT access token"""
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def decode_access_token(token: str) -> dict:
"""Decode and verify a JWT token"""
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return payload
except JWTError:
raise HTTPException(status_code=401, detail="Could not validate credentials")
async def get_current_admin(credentials: HTTPAuthorizationCredentials = Security(security)) -> dict:
"""Dependency to get current admin from JWT token"""
token = credentials.credentials
payload = decode_access_token(token)
email: str = payload.get("sub")
if email is None:
raise HTTPException(status_code=401, detail="Invalid authentication credentials")
return {"email": email}
+1
View File
@@ -0,0 +1 @@
# Models module
+85
View File
@@ -0,0 +1,85 @@
from pydantic import BaseModel, Field
from typing import Optional, List
from datetime import datetime
import uuid
class Destination(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
name: str
location: str
description: str
image: str
category: str # City, Beach, Adventure
rating: float
price: float
currency: str = "USD"
created_at: datetime = Field(default_factory=datetime.utcnow)
class DestinationCreate(BaseModel):
name: str
location: str
description: str
image: str
category: str
rating: float
price: float
currency: str = "USD"
class DestinationUpdate(BaseModel):
name: Optional[str] = None
location: Optional[str] = None
description: Optional[str] = None
image: Optional[str] = None
category: Optional[str] = None
rating: Optional[float] = None
price: Optional[float] = None
currency: Optional[str] = None
class Special(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
destination_id: str
discount: float
end_date: str # ISO format date
highlights: List[str]
created_at: datetime = Field(default_factory=datetime.utcnow)
class SpecialCreate(BaseModel):
destination_id: str
discount: float
end_date: str
highlights: List[str]
class SpecialUpdate(BaseModel):
discount: Optional[float] = None
end_date: Optional[str] = None
highlights: Optional[List[str]] = None
class AdminUser(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
email: str
password_hash: str
created_at: datetime = Field(default_factory=datetime.utcnow)
class AdminLogin(BaseModel):
email: str
password: str
class Contact(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
name: str
email: str
message: str
created_at: datetime = Field(default_factory=datetime.utcnow)
class ContactCreate(BaseModel):
name: str
email: str
message: str
class NewsletterSubscriber(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
email: str
subscribed_at: datetime = Field(default_factory=datetime.utcnow)
class NewsletterSubscribe(BaseModel):
email: str
+121 -25
View File
@@ -1,27 +1,123 @@
fastapi==0.110.1
uvicorn==0.25.0
boto3>=1.34.129
requests-oauthlib>=2.0.0
cryptography>=42.0.8
python-dotenv>=1.0.1
pymongo==4.5.0
pydantic>=2.6.4
email-validator>=2.2.0
pyjwt>=2.10.1
aiohappyeyeballs==2.6.1
aiohttp==3.13.3
aiosignal==1.4.0
annotated-doc==0.0.4
annotated-types==0.7.0
anyio==4.12.1
attrs==25.4.0
bcrypt==4.1.3
passlib>=1.7.4
tzdata>=2024.2
motor==3.3.1
pytest>=8.0.0
black>=24.1.1
isort>=5.13.2
flake8>=7.0.0
mypy>=1.8.0
python-jose>=3.3.0
requests>=2.31.0
pandas>=2.2.0
numpy>=1.26.0
python-multipart>=0.0.9
jq>=1.6.0
typer>=0.9.0
black==26.1.0
boto3==1.42.58
botocore==1.42.58
certifi==2026.2.25
cffi==2.0.0
charset-normalizer==3.4.4
click==8.3.1
cryptography==46.0.5
distro==1.9.0
dnspython==2.8.0
ecdsa==0.19.1
email-validator==2.3.0
emergentintegrations==0.1.0
fastapi==0.110.1
fastuuid==0.14.0
filelock==3.25.0
flake8==7.3.0
frozenlist==1.8.0
fsspec==2026.2.0
google-ai-generativelanguage==0.6.15
google-api-core==2.30.0
google-api-python-client==2.191.0
google-auth==2.49.0.dev0
google-auth-httplib2==0.3.0
google-genai==1.65.0
google-generativeai==0.8.6
googleapis-common-protos==1.72.0
grpcio==1.78.0
grpcio-status==1.71.2
h11==0.16.0
hf-xet==1.3.2
httpcore==1.0.9
httplib2==0.31.2
httpx==0.28.1
huggingface_hub==1.5.0
idna==3.11
importlib_metadata==8.7.1
iniconfig==2.3.0
isort==8.0.0
Jinja2==3.1.6
jiter==0.13.0
jmespath==1.1.0
jq==1.11.0
jsonschema==4.26.0
jsonschema-specifications==2025.9.1
librt==0.8.1
litellm==1.80.0
markdown-it-py==4.0.0
MarkupSafe==3.0.3
mccabe==0.7.0
mdurl==0.1.2
motor==3.3.1
multidict==6.7.1
mypy==1.19.1
mypy_extensions==1.1.0
numpy==2.4.2
oauthlib==3.3.1
openai==1.99.9
packaging==26.0
pandas==3.0.1
passlib==1.7.4
pathspec==1.0.4
pillow==12.1.1
platformdirs==4.9.2
pluggy==1.6.0
propcache==0.4.1
proto-plus==1.27.1
protobuf==5.29.6
pyasn1==0.6.2
pyasn1_modules==0.4.2
pycodestyle==2.14.0
pycparser==3.0
pydantic==2.12.5
pydantic_core==2.41.5
pyflakes==3.4.0
Pygments==2.19.2
PyJWT==2.11.0
pymongo==4.5.0
pyparsing==3.3.2
pytest==9.0.2
python-dateutil==2.9.0.post0
python-dotenv==1.2.1
python-jose==3.5.0
python-multipart==0.0.22
pytokens==0.4.1
PyYAML==6.0.3
referencing==0.37.0
regex==2026.2.28
requests==2.32.5
requests-oauthlib==2.0.0
rich==14.3.3
rpds-py==0.30.0
rsa==4.9.1
s3transfer==0.16.0
s5cmd==0.2.0
shellingham==1.5.4
six==1.17.0
sniffio==1.3.1
starlette==0.37.2
stripe==14.4.0
tenacity==9.1.4
tiktoken==0.12.0
tokenizers==0.22.2
tqdm==4.67.3
typer==0.24.1
typing-inspection==0.4.2
typing_extensions==4.15.0
tzdata==2025.3
uritemplate==4.2.0
urllib3==2.6.3
uvicorn==0.25.0
watchfiles==1.1.1
websockets==16.0
yarl==1.23.0
zipp==3.23.0
+1
View File
@@ -0,0 +1 @@
# Routes module
+61
View File
@@ -0,0 +1,61 @@
from fastapi import APIRouter, HTTPException, Depends
from models.schemas import AdminLogin
from auth import hash_password, verify_password, create_access_token
from motor.motor_asyncio import AsyncIOMotorClient
import os
router = APIRouter(prefix="/api/auth", tags=["Authentication"])
# MongoDB connection will be injected
db = None
def set_db(database):
global db
db = database
@router.post("/login")
async def login(credentials: AdminLogin):
"""Admin login endpoint"""
# Find admin user
admin = await db.admin_users.find_one({"email": credentials.email})
if not admin:
raise HTTPException(status_code=401, detail="Invalid email or password")
# Verify password
if not verify_password(credentials.password, admin["password_hash"]):
raise HTTPException(status_code=401, detail="Invalid email or password")
# Create access token
access_token = create_access_token(data={"sub": admin["email"]})
return {
"access_token": access_token,
"token_type": "bearer",
"email": admin["email"]
}
@router.post("/verify")
async def verify_token(admin: dict = Depends(lambda: __import__('auth').get_current_admin)):
"""Verify JWT token"""
return {"valid": True, "email": admin["email"]}
@router.post("/initialize-admin")
async def initialize_admin():
"""Initialize default admin user (for development/setup only)"""
# Check if admin already exists
existing_admin = await db.admin_users.find_one({"email": "admin@epictravel.com"})
if existing_admin:
return {"message": "Admin user already exists"}
# Create default admin
admin_data = {
"email": "admin@epictravel.com",
"password_hash": hash_password("admin123"),
"created_at": __import__('datetime').datetime.utcnow()
}
await db.admin_users.insert_one(admin_data)
return {"message": "Admin user created successfully", "email": "admin@epictravel.com"}
+113
View File
@@ -0,0 +1,113 @@
from fastapi import APIRouter, HTTPException, Depends
from typing import List, Optional
from models.schemas import Destination, DestinationCreate, DestinationUpdate
from auth import get_current_admin
import uuid
from datetime import datetime
router = APIRouter(prefix="/api/destinations", tags=["Destinations"])
# MongoDB connection will be injected
db = None
def set_db(database):
global db
db = database
@router.get("", response_model=List[Destination])
async def get_destinations(category: Optional[str] = None, search: Optional[str] = None):
"""Get all destinations with optional filtering"""
query = {}
if category and category != "All":
query["category"] = category
if search:
query["$or"] = [
{"name": {"$regex": search, "$options": "i"}},
{"location": {"$regex": search, "$options": "i"}}
]
destinations = await db.destinations.find(query).to_list(1000)
# Convert MongoDB _id to id for response
for dest in destinations:
if "_id" in dest:
del dest["_id"]
return destinations
@router.get("/{destination_id}", response_model=Destination)
async def get_destination(destination_id: str):
"""Get a single destination by ID"""
destination = await db.destinations.find_one({"id": destination_id})
if not destination:
raise HTTPException(status_code=404, detail="Destination not found")
if "_id" in destination:
del destination["_id"]
return destination
@router.post("", response_model=Destination)
async def create_destination(
destination: DestinationCreate,
admin: dict = Depends(get_current_admin)
):
"""Create a new destination (admin only)"""
destination_data = destination.dict()
destination_data["id"] = str(uuid.uuid4())
destination_data["created_at"] = datetime.utcnow()
await db.destinations.insert_one(destination_data)
if "_id" in destination_data:
del destination_data["_id"]
return destination_data
@router.put("/{destination_id}", response_model=Destination)
async def update_destination(
destination_id: str,
destination_update: DestinationUpdate,
admin: dict = Depends(get_current_admin)
):
"""Update a destination (admin only)"""
# Check if destination exists
existing = await db.destinations.find_one({"id": destination_id})
if not existing:
raise HTTPException(status_code=404, detail="Destination not found")
# Update only provided fields
update_data = {k: v for k, v in destination_update.dict().items() if v is not None}
if update_data:
await db.destinations.update_one(
{"id": destination_id},
{"$set": update_data}
)
# Fetch updated destination
updated = await db.destinations.find_one({"id": destination_id})
if "_id" in updated:
del updated["_id"]
return updated
@router.delete("/{destination_id}")
async def delete_destination(
destination_id: str,
admin: dict = Depends(get_current_admin)
):
"""Delete a destination (admin only)"""
result = await db.destinations.delete_one({"id": destination_id})
if result.deleted_count == 0:
raise HTTPException(status_code=404, detail="Destination not found")
# Also delete any specials for this destination
await db.specials.delete_many({"destination_id": destination_id})
return {"message": "Destination deleted successfully"}
+77
View File
@@ -0,0 +1,77 @@
from fastapi import APIRouter, HTTPException, File, UploadFile
from fastapi.responses import FileResponse
from typing import List
from models.schemas import ContactCreate, NewsletterSubscribe
from datetime import datetime
import uuid
import os
import shutil
router = APIRouter(prefix="/api", tags=["Other"])
# MongoDB connection will be injected
db = None
def set_db(database):
global db
db = database
@router.post("/contact")
async def submit_contact(contact: ContactCreate):
"""Submit a contact form"""
contact_data = contact.dict()
contact_data["id"] = str(uuid.uuid4())
contact_data["created_at"] = datetime.utcnow()
await db.contacts.insert_one(contact_data)
return {"message": "Contact form submitted successfully"}
@router.post("/newsletter/subscribe")
async def subscribe_newsletter(subscriber: NewsletterSubscribe):
"""Subscribe to newsletter"""
# Check if already subscribed
existing = await db.newsletter_subscribers.find_one({"email": subscriber.email})
if existing:
return {"message": "Email already subscribed"}
subscriber_data = subscriber.dict()
subscriber_data["id"] = str(uuid.uuid4())
subscriber_data["subscribed_at"] = datetime.utcnow()
await db.newsletter_subscribers.insert_one(subscriber_data)
return {"message": "Successfully subscribed to newsletter"}
@router.post("/upload/image")
async def upload_image(file: UploadFile = File(...)):
"""Upload an image file"""
# Validate file type
allowed_extensions = [".jpg", ".jpeg", ".png", ".webp"]
file_ext = os.path.splitext(file.filename)[1].lower()
if file_ext not in allowed_extensions:
raise HTTPException(status_code=400, detail="Invalid file type. Allowed: jpg, jpeg, png, webp")
# Generate unique filename
unique_filename = f"{uuid.uuid4()}{file_ext}"
file_path = f"/app/backend/uploads/{unique_filename}"
# Save file
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
# Return URL
file_url = f"/api/uploads/{unique_filename}"
return {"url": file_url, "filename": unique_filename}
@router.get("/uploads/{filename}")
async def get_uploaded_image(filename: str):
"""Serve uploaded images"""
file_path = f"/app/backend/uploads/{filename}"
if not os.path.exists(file_path):
raise HTTPException(status_code=404, detail="Image not found")
return FileResponse(file_path)
+109
View File
@@ -0,0 +1,109 @@
from fastapi import APIRouter, HTTPException, Depends
from typing import List
from models.schemas import Special, SpecialCreate, SpecialUpdate
from auth import get_current_admin
import uuid
from datetime import datetime
router = APIRouter(prefix="/api/specials", tags=["Specials"])
# MongoDB connection will be injected
db = None
def set_db(database):
global db
db = database
@router.get("", response_model=List[Special])
async def get_specials():
"""Get all weekly specials"""
specials = await db.specials.find().to_list(1000)
# Convert MongoDB _id to id for response
for special in specials:
if "_id" in special:
del special["_id"]
return specials
@router.get("/{special_id}", response_model=Special)
async def get_special(special_id: str):
"""Get a single special by ID"""
special = await db.specials.find_one({"id": special_id})
if not special:
raise HTTPException(status_code=404, detail="Special not found")
if "_id" in special:
del special["_id"]
return special
@router.post("", response_model=Special)
async def create_special(
special: SpecialCreate,
admin: dict = Depends(get_current_admin)
):
"""Add a destination to specials (admin only)"""
# Check if destination exists
destination = await db.destinations.find_one({"id": special.destination_id})
if not destination:
raise HTTPException(status_code=404, detail="Destination not found")
# Check if special already exists for this destination
existing = await db.specials.find_one({"destination_id": special.destination_id})
if existing:
raise HTTPException(status_code=400, detail="Special already exists for this destination")
special_data = special.dict()
special_data["id"] = str(uuid.uuid4())
special_data["created_at"] = datetime.utcnow()
await db.specials.insert_one(special_data)
if "_id" in special_data:
del special_data["_id"]
return special_data
@router.put("/{special_id}", response_model=Special)
async def update_special(
special_id: str,
special_update: SpecialUpdate,
admin: dict = Depends(get_current_admin)
):
"""Update a special (admin only)"""
# Check if special exists
existing = await db.specials.find_one({"id": special_id})
if not existing:
raise HTTPException(status_code=404, detail="Special not found")
# Update only provided fields
update_data = {k: v for k, v in special_update.dict().items() if v is not None}
if update_data:
await db.specials.update_one(
{"id": special_id},
{"$set": update_data}
)
# Fetch updated special
updated = await db.specials.find_one({"id": special_id})
if "_id" in updated:
del updated["_id"]
return updated
@router.delete("/destination/{destination_id}")
async def delete_special_by_destination(
destination_id: str,
admin: dict = Depends(get_current_admin)
):
"""Remove a destination from specials (admin only)"""
result = await db.specials.delete_one({"destination_id": destination_id})
if result.deleted_count == 0:
raise HTTPException(status_code=404, detail="Special not found for this destination")
return {"message": "Special removed successfully"}
+224 -52
View File
@@ -1,15 +1,15 @@
from fastapi import FastAPI, APIRouter
from fastapi import FastAPI
from dotenv import load_dotenv
from starlette.middleware.cors import CORSMiddleware
from motor.motor_asyncio import AsyncIOMotorClient
import os
import logging
from pathlib import Path
from pydantic import BaseModel, Field, ConfigDict
from typing import List
import uuid
from datetime import datetime, timezone
from auth import hash_password
from datetime import datetime
# Import route modules
from routes import auth_routes, destination_routes, special_routes, other_routes
ROOT_DIR = Path(__file__).parent
load_dotenv(ROOT_DIR / '.env')
@@ -19,60 +19,30 @@ mongo_url = os.environ['MONGO_URL']
client = AsyncIOMotorClient(mongo_url)
db = client[os.environ['DB_NAME']]
# Create the main app without a prefix
app = FastAPI()
# Inject database into route modules
auth_routes.set_db(db)
destination_routes.set_db(db)
special_routes.set_db(db)
other_routes.set_db(db)
# Create a router with the /api prefix
api_router = APIRouter(prefix="/api")
# Create the main app
app = FastAPI(title="Epic Travel & Destinations API")
# Include routers
app.include_router(auth_routes.router)
app.include_router(destination_routes.router)
app.include_router(special_routes.router)
app.include_router(other_routes.router)
# Define Models
class StatusCheck(BaseModel):
model_config = ConfigDict(extra="ignore") # Ignore MongoDB's _id field
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
client_name: str
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class StatusCheckCreate(BaseModel):
client_name: str
# Add your routes to the router instead of directly to app
@api_router.get("/")
# Health check endpoint
@app.get("/api")
async def root():
return {"message": "Hello World"}
@api_router.post("/status", response_model=StatusCheck)
async def create_status_check(input: StatusCheckCreate):
status_dict = input.model_dump()
status_obj = StatusCheck(**status_dict)
# Convert to dict and serialize datetime to ISO string for MongoDB
doc = status_obj.model_dump()
doc['timestamp'] = doc['timestamp'].isoformat()
_ = await db.status_checks.insert_one(doc)
return status_obj
@api_router.get("/status", response_model=List[StatusCheck])
async def get_status_checks():
# Exclude MongoDB's _id field from the query results
status_checks = await db.status_checks.find({}, {"_id": 0}).to_list(1000)
# Convert ISO string timestamps back to datetime objects
for check in status_checks:
if isinstance(check['timestamp'], str):
check['timestamp'] = datetime.fromisoformat(check['timestamp'])
return status_checks
# Include the router in the main app
app.include_router(api_router)
return {"message": "Epic Travel API is running", "status": "healthy"}
app.add_middleware(
CORSMiddleware,
allow_credentials=True,
allow_origins=os.environ.get('CORS_ORIGINS', '*').split(','),
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
@@ -84,6 +54,208 @@ logging.basicConfig(
)
logger = logging.getLogger(__name__)
@app.on_event("startup")
async def startup_db_client():
"""Initialize database with seed data if empty"""
try:
# Check if admin user exists, if not create one
admin_exists = await db.admin_users.find_one({"email": "admin@epictravel.com"})
if not admin_exists:
admin_data = {
"id": "admin-1",
"email": "admin@epictravel.com",
"password_hash": hash_password("admin123"),
"created_at": datetime.utcnow()
}
await db.admin_users.insert_one(admin_data)
logger.info("Default admin user created")
# Check if destinations exist, if not seed initial data
dest_count = await db.destinations.count_documents({})
if dest_count == 0:
# Seed initial destinations
initial_destinations = [
{
"id": "1",
"name": "Paris",
"location": "France",
"description": "Experience the romance and elegance of the City of Light. Visit iconic landmarks like the Eiffel Tower, Louvre Museum, and stroll along the Champs-Élysées.",
"image": "https://images.unsplash.com/photo-1502602898657-3e91760cbb34?w=800&q=80",
"category": "City",
"rating": 4.9,
"price": 1299,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "2",
"name": "Bali",
"location": "Indonesia",
"description": "Discover tropical paradise with stunning beaches, ancient temples, lush rice terraces, and vibrant culture in this Indonesian gem.",
"image": "https://images.unsplash.com/photo-1537996194471-e657df975ab4?w=800&q=80",
"category": "Beach",
"rating": 4.8,
"price": 899,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "3",
"name": "Tokyo",
"location": "Japan",
"description": "Immerse yourself in the perfect blend of ancient tradition and cutting-edge technology in Japan's bustling capital city.",
"image": "https://images.unsplash.com/photo-1540959733332-eab4deabeeaf?w=800&q=80",
"category": "City",
"rating": 4.9,
"price": 1499,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "4",
"name": "Santorini",
"location": "Greece",
"description": "Marvel at breathtaking sunsets, whitewashed buildings, and crystal-clear waters in this stunning Greek island paradise.",
"image": "https://images.unsplash.com/photo-1613395877344-13d4a8e0d49e?w=800&q=80",
"category": "Beach",
"rating": 4.9,
"price": 1199,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "5",
"name": "Iceland",
"location": "Iceland",
"description": "Witness the Northern Lights, explore glaciers, geysers, and volcanic landscapes in this land of fire and ice.",
"image": "https://images.unsplash.com/photo-1504829857797-ddff29c27927?w=800&q=80",
"category": "Adventure",
"rating": 4.8,
"price": 1699,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "6",
"name": "Dubai",
"location": "UAE",
"description": "Experience luxury and innovation in the desert with world-class shopping, stunning architecture, and endless entertainment.",
"image": "https://images.unsplash.com/photo-1512453979798-5ea266f8880c?w=800&q=80",
"category": "City",
"rating": 4.7,
"price": 1399,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "7",
"name": "Maldives",
"location": "Maldives",
"description": "Relax in overwater bungalows, dive in pristine coral reefs, and enjoy the ultimate tropical island getaway.",
"image": "https://images.unsplash.com/photo-1514282401047-d79a71a590e8?w=800&q=80",
"category": "Beach",
"rating": 5.0,
"price": 2199,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "8",
"name": "New York",
"location": "USA",
"description": "Explore the city that never sleeps with iconic landmarks, world-class museums, Broadway shows, and diverse neighborhoods.",
"image": "https://images.unsplash.com/photo-1496442226666-8d4d0e62e6e9?w=800&q=80",
"category": "City",
"rating": 4.8,
"price": 1099,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "9",
"name": "Machu Picchu",
"location": "Peru",
"description": "Trek to the ancient Incan citadel nestled high in the Andes Mountains, one of the New Seven Wonders of the World.",
"image": "https://images.unsplash.com/photo-1587595431973-160d0d94add1?w=800&q=80",
"category": "Adventure",
"rating": 4.9,
"price": 1299,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "10",
"name": "Swiss Alps",
"location": "Switzerland",
"description": "Ski pristine slopes, hike mountain trails, and enjoy charming alpine villages with breathtaking mountain vistas.",
"image": "https://images.unsplash.com/photo-1531366936337-7c912a4589a7?w=800&q=80",
"category": "Adventure",
"rating": 4.9,
"price": 1799,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "11",
"name": "Venice",
"location": "Italy",
"description": "Glide through romantic canals, admire Renaissance architecture, and savor authentic Italian cuisine in this unique floating city.",
"image": "https://images.unsplash.com/photo-1523906834658-6e24ef2386f9?w=800&q=80",
"category": "City",
"rating": 4.8,
"price": 1149,
"currency": "USD",
"created_at": datetime.utcnow()
},
{
"id": "12",
"name": "Safari Kenya",
"location": "Kenya",
"description": "Witness the Great Migration, spot the Big Five, and experience the raw beauty of African wilderness.",
"image": "https://images.unsplash.com/photo-1516426122078-c23e76319801?w=800&q=80",
"category": "Adventure",
"rating": 4.9,
"price": 2499,
"currency": "USD",
"created_at": datetime.utcnow()
}
]
await db.destinations.insert_many(initial_destinations)
logger.info(f"Seeded {len(initial_destinations)} initial destinations")
# Seed initial specials
initial_specials = [
{
"id": "special-1",
"destination_id": "2",
"discount": 25,
"end_date": "2025-02-28",
"highlights": ["Free spa treatment", "Complimentary airport transfer", "Sunset dinner cruise"],
"created_at": datetime.utcnow()
},
{
"id": "special-2",
"destination_id": "4",
"discount": 30,
"end_date": "2025-03-15",
"highlights": ["Wine tasting tour", "Private yacht excursion", "Luxury accommodation upgrade"],
"created_at": datetime.utcnow()
},
{
"id": "special-3",
"destination_id": "7",
"discount": 20,
"end_date": "2025-02-20",
"highlights": ["Snorkeling adventure", "Couples massage", "Romantic beach dinner"],
"created_at": datetime.utcnow()
}
]
await db.specials.insert_many(initial_specials)
logger.info(f"Seeded {len(initial_specials)} initial specials")
except Exception as e:
logger.error(f"Error during startup: {str(e)}")
@app.on_event("shutdown")
async def shutdown_db_client():
client.close()
client.close()