update to python fastpi

This commit is contained in:
Iliyan Angelov
2025-11-16 15:59:05 +02:00
parent 93d4c1df80
commit 98ccd5b6ff
4464 changed files with 773233 additions and 13740 deletions

35
Backend/.env.example Normal file
View File

@@ -0,0 +1,35 @@
# Environment
NODE_ENV=development
# Server
PORT=3000
HOST=localhost
# Database
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASS=
DB_NAME=hotel_booking_dev
# JWT
JWT_SECRET=your_super_secret_jwt_key_change_this_in_production
JWT_EXPIRES_IN=1h
JWT_REFRESH_SECRET=your_super_secret_refresh_key_change_this_in_production
JWT_REFRESH_EXPIRES_IN=7d
# Client URL
CLIENT_URL=http://localhost:5173
# Upload
MAX_FILE_SIZE=5242880
ALLOWED_FILE_TYPES=image/jpeg,image/png,image/jpg,image/webp
# Pagination
DEFAULT_PAGE_SIZE=10
MAX_PAGE_SIZE=100
# Rate Limiting
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100

42
Backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,42 @@
# Dependencies
node_modules/
package-lock.json
yarn.lock
# Environment variables
.env
.env.local
.env.*.local
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Uploads
uploads/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Testing
coverage/
.nyc_output/
# Build
dist/
build/
# Temporary files
tmp/
temp/

8
Backend/.sequelizerc Normal file
View File

@@ -0,0 +1,8 @@
const path = require('path');
module.exports = {
'config': path.resolve('src', 'config', 'database.js'),
'models-path': path.resolve('src/databases', 'models'),
'seeders-path': path.resolve('src/databases', 'seeders'),
'migrations-path': path.resolve('src/databases', 'migrations')
};

115
Backend/alembic.ini Normal file
View File

@@ -0,0 +1,115 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = alembic
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python-dateutil library that can be
# installed by adding `alembic[tz]` to the pip requirements
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to alembic/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = driver://user:pass@localhost/dbname
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
# hooks = ruff
# ruff.type = exec
# ruff.executable = %(here)s/.venv/bin/ruff
# ruff.options = --fix REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

73
Backend/alembic/env.py Normal file
View File

@@ -0,0 +1,73 @@
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
import os
import sys
from pathlib import Path
# Add parent directory to path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
# Import models and Base
from src.config.database import Base
from src.models import * # Import all models
# this is the Alembic Config object
config = context.config
# Interpret the config file for Python logging.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# Get database URL from environment
database_url = os.getenv("DATABASE_URL")
if not database_url:
db_user = os.getenv("DB_USER", "root")
db_pass = os.getenv("DB_PASS", "")
db_name = os.getenv("DB_NAME", "hotel_db")
db_host = os.getenv("DB_HOST", "localhost")
db_port = os.getenv("DB_PORT", "3306")
database_url = f"mysql+pymysql://{db_user}:{db_pass}@{db_host}:{db_port}/{db_name}"
config.set_main_option("sqlalchemy.url", database_url)
# add your model's MetaData object here
target_metadata = Base.metadata
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode."""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode."""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection, target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View File

@@ -0,0 +1,25 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}

19
Backend/requirements.txt Normal file
View File

@@ -0,0 +1,19 @@
fastapi==0.104.1
uvicorn[standard]==0.24.0
python-dotenv==1.0.0
sqlalchemy==2.0.23
pymysql==1.1.0
cryptography==41.0.7
python-jose[cryptography]==3.3.0
bcrypt==4.1.2
python-multipart==0.0.6
aiofiles==23.2.1
email-validator==2.1.0
pydantic==2.5.0
pydantic-settings==2.1.0
slowapi==0.1.9
pillow==10.1.0
aiosmtplib==3.0.1
jinja2==3.1.2
alembic==1.12.1

23
Backend/run.py Normal file
View File

@@ -0,0 +1,23 @@
#!/usr/bin/env python3
"""
Main entry point for the FastAPI server
"""
import uvicorn
import os
from dotenv import load_dotenv
load_dotenv()
if __name__ == "__main__":
port = int(os.getenv("PORT", 8000))
host = os.getenv("HOST", "0.0.0.0")
reload = os.getenv("NODE_ENV") == "development"
uvicorn.run(
"src.main:app",
host=host,
port=port,
reload=reload,
log_level="info"
)

2
Backend/src/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
# Hotel Booking Server Package

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,38 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import os
from dotenv import load_dotenv
load_dotenv()
# Database configuration
DB_USER = os.getenv("DB_USER", "root")
DB_PASS = os.getenv("DB_PASS", "")
DB_NAME = os.getenv("DB_NAME", "hotel_db")
DB_HOST = os.getenv("DB_HOST", "localhost")
DB_PORT = os.getenv("DB_PORT", "3306")
DATABASE_URL = f"mysql+pymysql://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
engine = create_engine(
DATABASE_URL,
pool_pre_ping=True,
pool_recycle=300,
pool_size=5,
max_overflow=10,
echo=os.getenv("NODE_ENV") == "development"
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# Dependency to get DB session
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

121
Backend/src/main.py Normal file
View File

@@ -0,0 +1,121 @@
from fastapi import FastAPI, Request, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from sqlalchemy.exc import IntegrityError
from jose.exceptions import JWTError
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
import os
from pathlib import Path
from .config.database import engine, Base
from .middleware.error_handler import (
validation_exception_handler,
integrity_error_handler,
jwt_error_handler,
http_exception_handler,
general_exception_handler
)
# Create database tables
Base.metadata.create_all(bind=engine)
from .routes import auth_routes
# Initialize FastAPI app
app = FastAPI(
title="Hotel Booking API",
description="Hotel booking backend API",
version="1.0.0"
)
# Rate limiting
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
# CORS configuration
# Allow multiple origins for development
client_url = os.getenv("CLIENT_URL", "http://localhost:5173")
allowed_origins = [
client_url,
"http://localhost:5173", # Vite default
"http://localhost:3000", # Alternative port
"http://localhost:5174", # Vite alternative
"http://127.0.0.1:5173",
"http://127.0.0.1:3000",
"http://127.0.0.1:5174",
]
# In development, allow all localhost origins using regex
if os.getenv("ENVIRONMENT", "development") == "development":
# For development, use regex to allow any localhost port
app.add_middleware(
CORSMiddleware,
allow_origin_regex=r"http://(localhost|127\.0\.0\.1)(:\d+)?",
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
else:
# Production: use specific origins
app.add_middleware(
CORSMiddleware,
allow_origins=allowed_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Serve static files (uploads)
uploads_dir = Path(__file__).parent.parent / "uploads"
uploads_dir.mkdir(exist_ok=True)
app.mount("/uploads", StaticFiles(directory=str(uploads_dir)), name="uploads")
# Exception handlers
app.add_exception_handler(HTTPException, http_exception_handler)
app.add_exception_handler(RequestValidationError, validation_exception_handler)
app.add_exception_handler(IntegrityError, integrity_error_handler)
app.add_exception_handler(JWTError, jwt_error_handler)
app.add_exception_handler(Exception, general_exception_handler)
# Health check
@app.get("/health")
async def health_check():
return {
"status": "success",
"message": "Server is running",
"timestamp": __import__("datetime").datetime.utcnow().isoformat()
}
# API Routes
app.include_router(auth_routes.router, prefix="/api")
# Import and include other routes
from .routes import (
room_routes, booking_routes, payment_routes, banner_routes,
favorite_routes, service_routes, promotion_routes, report_routes,
review_routes, user_routes
)
app.include_router(room_routes.router, prefix="/api")
app.include_router(booking_routes.router, prefix="/api")
app.include_router(payment_routes.router, prefix="/api")
app.include_router(banner_routes.router, prefix="/api")
app.include_router(favorite_routes.router, prefix="/api")
app.include_router(service_routes.router, prefix="/api")
app.include_router(promotion_routes.router, prefix="/api")
app.include_router(report_routes.router, prefix="/api")
app.include_router(review_routes.router, prefix="/api")
app.include_router(user_routes.router, prefix="/api")
# Note: FastAPI automatically handles 404s for unmatched routes
# This handler is kept for custom 404 responses but may not be needed
if __name__ == "__main__":
import uvicorn
port = int(os.getenv("PORT", 3000))
uvicorn.run("main:app", host="0.0.0.0", port=port, reload=True)

View File

@@ -0,0 +1,61 @@
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import JWTError, jwt
from sqlalchemy.orm import Session
from typing import Optional
import os
from ..config.database import get_db
from ..models.user import User
security = HTTPBearer()
def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security),
db: Session = Depends(get_db)
) -> User:
"""
Verify JWT token and return current user
"""
token = credentials.credentials
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, os.getenv("JWT_SECRET"), algorithms=["HS256"])
user_id: int = payload.get("userId")
if user_id is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = db.query(User).filter(User.id == user_id).first()
if user is None:
raise credentials_exception
return user
def authorize_roles(*allowed_roles: str):
"""
Check if user has required role
"""
def role_checker(current_user: User = Depends(get_current_user)) -> User:
# Map role IDs to role names
role_map = {1: "admin", 2: "staff", 3: "customer"}
user_role_name = role_map.get(current_user.role_id)
if user_role_name not in allowed_roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You do not have permission to access this resource"
)
return current_user
return role_checker

View File

@@ -0,0 +1,127 @@
from fastapi import Request, status, HTTPException
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from sqlalchemy.exc import IntegrityError
from jose.exceptions import JWTError
import os
import traceback
async def validation_exception_handler(request: Request, exc: RequestValidationError):
"""
Handle validation errors
"""
errors = []
for error in exc.errors():
field = ".".join(str(loc) for loc in error["loc"] if loc != "body")
errors.append({
"field": field,
"message": error["msg"]
})
# Get the first error message for the main message
first_error = errors[0]["message"] if errors else "Validation error"
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={
"status": "error",
"message": first_error,
"errors": errors
}
)
async def integrity_error_handler(request: Request, exc: IntegrityError):
"""
Handle database integrity errors (unique constraints, etc.)
"""
error_msg = str(exc.orig) if hasattr(exc, 'orig') else str(exc)
# Check for duplicate entry
if "Duplicate entry" in error_msg or "UNIQUE constraint" in error_msg:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={
"status": "error",
"message": "Duplicate entry",
"errors": [{"message": "This record already exists"}]
}
)
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={
"status": "error",
"message": "Database integrity error"
}
)
async def jwt_error_handler(request: Request, exc: JWTError):
"""
Handle JWT errors
"""
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content={
"status": "error",
"message": "Invalid token"
}
)
async def http_exception_handler(request: Request, exc: HTTPException):
"""
Handle HTTPException errors
"""
# If detail is already a dict with status/message, return it directly
if isinstance(exc.detail, dict):
return JSONResponse(
status_code=exc.status_code,
content=exc.detail
)
# Otherwise format as standard error response
return JSONResponse(
status_code=exc.status_code,
content={
"status": "error",
"message": str(exc.detail) if exc.detail else "An error occurred"
}
)
async def general_exception_handler(request: Request, exc: Exception):
"""
Handle all other exceptions
"""
# Log error
print(f"Error: {exc}")
if os.getenv("NODE_ENV") == "development":
traceback.print_exc()
# Handle HTTPException with dict detail
if isinstance(exc, Exception) and hasattr(exc, "status_code"):
status_code = exc.status_code
if hasattr(exc, "detail"):
detail = exc.detail
if isinstance(detail, dict):
# If detail is already a dict with status/message, return it directly
return JSONResponse(status_code=status_code, content=detail)
message = str(detail) if detail else "An error occurred"
else:
message = str(exc) if str(exc) else "Internal server error"
else:
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
message = str(exc) if str(exc) else "Internal server error"
return JSONResponse(
status_code=status_code,
content={
"status": "error",
"message": message,
**({"stack": traceback.format_exc()} if os.getenv("NODE_ENV") == "development" else {})
}
)

View File

@@ -0,0 +1,34 @@
from .role import Role
from .user import User
from .refresh_token import RefreshToken
from .password_reset_token import PasswordResetToken
from .room_type import RoomType
from .room import Room
from .booking import Booking
from .payment import Payment
from .service import Service
from .service_usage import ServiceUsage
from .promotion import Promotion
from .checkin_checkout import CheckInCheckOut
from .banner import Banner
from .review import Review
from .favorite import Favorite
__all__ = [
"Role",
"User",
"RefreshToken",
"PasswordResetToken",
"RoomType",
"Room",
"Booking",
"Payment",
"Service",
"ServiceUsage",
"Promotion",
"CheckInCheckOut",
"Banner",
"Review",
"Favorite",
]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,31 @@
from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text
from sqlalchemy.orm import relationship
from datetime import datetime
from ..config.database import Base
class Banner(Base):
__tablename__ = "banners"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
title = Column(String(100), nullable=False)
description = Column(Text, nullable=True)
image_url = Column(String(255), nullable=False)
link_url = Column(String(255), nullable=True)
position = Column(String(50), nullable=False, default="home")
display_order = Column(Integer, nullable=False, default=0)
is_active = Column(Boolean, nullable=False, default=True)
start_date = Column(DateTime, nullable=True)
end_date = Column(DateTime, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
def is_active_now(self):
from datetime import datetime
now = datetime.utcnow()
if not self.is_active:
return False
if not self.start_date or not self.end_date:
return self.is_active
return self.start_date <= now <= self.end_date

View File

@@ -0,0 +1,40 @@
from sqlalchemy import Column, Integer, String, DateTime, Numeric, Boolean, Text, Enum, ForeignKey
from sqlalchemy.orm import relationship
from datetime import datetime
import enum
from ..config.database import Base
class BookingStatus(str, enum.Enum):
pending = "pending"
confirmed = "confirmed"
checked_in = "checked_in"
checked_out = "checked_out"
cancelled = "cancelled"
class Booking(Base):
__tablename__ = "bookings"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
booking_number = Column(String(50), unique=True, nullable=False, index=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
room_id = Column(Integer, ForeignKey("rooms.id"), nullable=False)
check_in_date = Column(DateTime, nullable=False)
check_out_date = Column(DateTime, nullable=False)
num_guests = Column(Integer, nullable=False, default=1)
total_price = Column(Numeric(10, 2), nullable=False)
status = Column(Enum(BookingStatus), nullable=False, default=BookingStatus.pending)
deposit_paid = Column(Boolean, nullable=False, default=False)
requires_deposit = Column(Boolean, nullable=False, default=False)
special_requests = Column(Text, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
# Relationships
user = relationship("User", back_populates="bookings")
room = relationship("Room", back_populates="bookings")
payments = relationship("Payment", back_populates="booking", cascade="all, delete-orphan")
service_usages = relationship("ServiceUsage", back_populates="booking", cascade="all, delete-orphan")
checkin_checkout = relationship("CheckInCheckOut", back_populates="booking", uselist=False)

View File

@@ -0,0 +1,27 @@
from sqlalchemy import Column, Integer, DateTime, Numeric, Text, ForeignKey
from sqlalchemy.orm import relationship
from datetime import datetime
from ..config.database import Base
class CheckInCheckOut(Base):
__tablename__ = "checkin_checkout"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
booking_id = Column(Integer, ForeignKey("bookings.id"), nullable=False, unique=True)
checkin_time = Column(DateTime, nullable=True)
checkout_time = Column(DateTime, nullable=True)
checkin_by = Column(Integer, ForeignKey("users.id"), nullable=True)
checkout_by = Column(Integer, ForeignKey("users.id"), nullable=True)
room_condition_checkin = Column(Text, nullable=True)
room_condition_checkout = Column(Text, nullable=True)
additional_charges = Column(Numeric(10, 2), nullable=False, default=0.0)
notes = Column(Text, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
# Relationships
booking = relationship("Booking", back_populates="checkin_checkout")
checked_in_by = relationship("User", foreign_keys=[checkin_by], back_populates="checkins_processed")
checked_out_by = relationship("User", foreign_keys=[checkout_by], back_populates="checkouts_processed")

View File

@@ -0,0 +1,19 @@
from sqlalchemy import Column, Integer, ForeignKey, DateTime
from sqlalchemy.orm import relationship
from datetime import datetime
from ..config.database import Base
class Favorite(Base):
__tablename__ = "favorites"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
room_id = Column(Integer, ForeignKey("rooms.id"), nullable=False)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
# Relationships
user = relationship("User", back_populates="favorites")
room = relationship("Room", back_populates="favorites")

View File

@@ -0,0 +1,20 @@
from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey
from sqlalchemy.orm import relationship
from datetime import datetime
from ..config.database import Base
class PasswordResetToken(Base):
__tablename__ = "password_reset_tokens"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
token = Column(String(255), unique=True, nullable=False, index=True)
expires_at = Column(DateTime, nullable=False)
used = Column(Boolean, default=False, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
# Relationships
user = relationship("User")

View File

@@ -0,0 +1,49 @@
from sqlalchemy import Column, Integer, String, DateTime, Numeric, Text, Enum, ForeignKey
from sqlalchemy.orm import relationship
from datetime import datetime
import enum
from ..config.database import Base
class PaymentMethod(str, enum.Enum):
cash = "cash"
credit_card = "credit_card"
debit_card = "debit_card"
bank_transfer = "bank_transfer"
e_wallet = "e_wallet"
class PaymentType(str, enum.Enum):
full = "full"
deposit = "deposit"
remaining = "remaining"
class PaymentStatus(str, enum.Enum):
pending = "pending"
completed = "completed"
failed = "failed"
refunded = "refunded"
class Payment(Base):
__tablename__ = "payments"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
booking_id = Column(Integer, ForeignKey("bookings.id"), nullable=False)
amount = Column(Numeric(10, 2), nullable=False)
payment_method = Column(Enum(PaymentMethod), nullable=False)
payment_type = Column(Enum(PaymentType), nullable=False, default=PaymentType.full)
deposit_percentage = Column(Integer, nullable=True)
related_payment_id = Column(Integer, ForeignKey("payments.id"), nullable=True)
payment_status = Column(Enum(PaymentStatus), nullable=False, default=PaymentStatus.pending)
transaction_id = Column(String(100), nullable=True)
payment_date = Column(DateTime, nullable=True)
notes = Column(Text, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
# Relationships
booking = relationship("Booking", back_populates="payments")
related_payment = relationship("Payment", remote_side=[id], backref="related_payments")

View File

@@ -0,0 +1,60 @@
from sqlalchemy import Column, Integer, String, DateTime, Numeric, Boolean, Text, Enum
from sqlalchemy.orm import relationship
from datetime import datetime
import enum
from ..config.database import Base
class DiscountType(str, enum.Enum):
percentage = "percentage"
fixed_amount = "fixed_amount"
class Promotion(Base):
__tablename__ = "promotions"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
code = Column(String(50), unique=True, nullable=False, index=True)
name = Column(String(100), nullable=False)
description = Column(Text, nullable=True)
discount_type = Column(Enum(DiscountType), nullable=False)
discount_value = Column(Numeric(10, 2), nullable=False)
min_booking_amount = Column(Numeric(10, 2), nullable=True)
max_discount_amount = Column(Numeric(10, 2), nullable=True)
start_date = Column(DateTime, nullable=False)
end_date = Column(DateTime, nullable=False)
usage_limit = Column(Integer, nullable=True)
used_count = Column(Integer, nullable=False, default=0)
is_active = Column(Boolean, nullable=False, default=True)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
def is_valid(self):
from datetime import datetime
now = datetime.utcnow()
if not self.is_active:
return False
if now < self.start_date or now > self.end_date:
return False
if self.usage_limit is not None and self.used_count >= self.usage_limit:
return False
return True
def calculate_discount(self, booking_amount):
if not self.is_valid():
return 0.0
if self.min_booking_amount and booking_amount < float(self.min_booking_amount):
return 0.0
discount = 0.0
if self.discount_type == DiscountType.percentage:
discount = float(booking_amount) * float(self.discount_value) / 100.0
elif self.discount_type == DiscountType.fixed_amount:
discount = float(self.discount_value)
if self.max_discount_amount and discount > float(self.max_discount_amount):
discount = float(self.max_discount_amount)
return discount

View File

@@ -0,0 +1,18 @@
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from datetime import datetime
from ..config.database import Base
class RefreshToken(Base):
__tablename__ = "refresh_tokens"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
token = Column(String(500), unique=True, nullable=False, index=True)
expires_at = Column(DateTime, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
# Relationships
user = relationship("User", back_populates="refresh_tokens")

View File

@@ -0,0 +1,29 @@
from sqlalchemy import Column, Integer, Text, Enum, ForeignKey, DateTime
from sqlalchemy.orm import relationship
from datetime import datetime
import enum
from ..config.database import Base
class ReviewStatus(str, enum.Enum):
pending = "pending"
approved = "approved"
rejected = "rejected"
class Review(Base):
__tablename__ = "reviews"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
room_id = Column(Integer, ForeignKey("rooms.id"), nullable=False)
rating = Column(Integer, nullable=False)
comment = Column(Text, nullable=False)
status = Column(Enum(ReviewStatus), nullable=False, default=ReviewStatus.pending)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
# Relationships
user = relationship("User", back_populates="reviews")
room = relationship("Room", back_populates="reviews")

View File

@@ -0,0 +1,18 @@
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.orm import relationship
from datetime import datetime
from ..config.database import Base
class Role(Base):
__tablename__ = "roles"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
name = Column(String(50), unique=True, nullable=False, index=True)
description = Column(String(255), nullable=True)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
# Relationships
users = relationship("User", back_populates="role")

View File

@@ -0,0 +1,36 @@
from sqlalchemy import Column, Integer, String, Numeric, Boolean, Text, JSON, Enum, ForeignKey, DateTime
from sqlalchemy.orm import relationship
from datetime import datetime
import enum
from ..config.database import Base
class RoomStatus(str, enum.Enum):
available = "available"
occupied = "occupied"
maintenance = "maintenance"
cleaning = "cleaning"
class Room(Base):
__tablename__ = "rooms"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
room_type_id = Column(Integer, ForeignKey("room_types.id"), nullable=False)
room_number = Column(String(20), unique=True, nullable=False, index=True)
floor = Column(Integer, nullable=False)
status = Column(Enum(RoomStatus), nullable=False, default=RoomStatus.available)
price = Column(Numeric(10, 2), nullable=False)
featured = Column(Boolean, nullable=False, default=False)
images = Column(JSON, nullable=True)
amenities = Column(JSON, nullable=True)
description = Column(Text, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
# Relationships
room_type = relationship("RoomType", back_populates="rooms")
bookings = relationship("Booking", back_populates="room")
reviews = relationship("Review", back_populates="room")
favorites = relationship("Favorite", back_populates="room", cascade="all, delete-orphan")

View File

@@ -0,0 +1,21 @@
from sqlalchemy import Column, Integer, String, Numeric, Text, JSON, DateTime
from sqlalchemy.orm import relationship
from datetime import datetime
from ..config.database import Base
class RoomType(Base):
__tablename__ = "room_types"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
name = Column(String(100), unique=True, nullable=False)
description = Column(Text, nullable=True)
base_price = Column(Numeric(10, 2), nullable=False)
capacity = Column(Integer, nullable=False)
amenities = Column(JSON, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
# Relationships
rooms = relationship("Room", back_populates="room_type")

View File

@@ -0,0 +1,21 @@
from sqlalchemy import Column, Integer, String, Numeric, Boolean, Text, DateTime
from sqlalchemy.orm import relationship
from datetime import datetime
from ..config.database import Base
class Service(Base):
__tablename__ = "services"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
name = Column(String(100), nullable=False)
description = Column(Text, nullable=True)
price = Column(Numeric(10, 2), nullable=False)
category = Column(String(50), nullable=True)
is_active = Column(Boolean, nullable=False, default=True)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
# Relationships
service_usages = relationship("ServiceUsage", back_populates="service")

View File

@@ -0,0 +1,24 @@
from sqlalchemy import Column, Integer, DateTime, Numeric, Text, ForeignKey
from sqlalchemy.orm import relationship
from datetime import datetime
from ..config.database import Base
class ServiceUsage(Base):
__tablename__ = "service_usages"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
booking_id = Column(Integer, ForeignKey("bookings.id"), nullable=False)
service_id = Column(Integer, ForeignKey("services.id"), nullable=False)
quantity = Column(Integer, nullable=False, default=1)
unit_price = Column(Numeric(10, 2), nullable=False)
total_price = Column(Numeric(10, 2), nullable=False)
usage_date = Column(DateTime, nullable=False, default=datetime.utcnow)
notes = Column(Text, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
# Relationships
booking = relationship("Booking", back_populates="service_usages")
service = relationship("Service", back_populates="service_usages")

View File

@@ -0,0 +1,30 @@
from sqlalchemy import Column, Integer, String, Boolean, Text, ForeignKey, DateTime
from sqlalchemy.orm import relationship
from datetime import datetime
from ..config.database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
role_id = Column(Integer, ForeignKey("roles.id"), nullable=False)
email = Column(String(100), unique=True, nullable=False, index=True)
password = Column(String(255), nullable=False)
full_name = Column(String(100), nullable=False)
phone = Column(String(20), nullable=True)
address = Column(Text, nullable=True)
avatar = Column(String(255), nullable=True)
is_active = Column(Boolean, nullable=False, default=True)
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
# Relationships
role = relationship("Role", back_populates="users")
bookings = relationship("Booking", back_populates="user")
refresh_tokens = relationship("RefreshToken", back_populates="user", cascade="all, delete-orphan")
checkins_processed = relationship("CheckInCheckOut", foreign_keys="CheckInCheckOut.checkin_by", back_populates="checked_in_by")
checkouts_processed = relationship("CheckInCheckOut", foreign_keys="CheckInCheckOut.checkout_by", back_populates="checked_out_by")
reviews = relationship("Review", back_populates="user")
favorites = relationship("Favorite", back_populates="user", cascade="all, delete-orphan")

View File

@@ -0,0 +1,2 @@
# Routes package

View File

@@ -0,0 +1,216 @@
from fastapi import APIRouter, Depends, HTTPException, status, Cookie, Response
from fastapi.responses import JSONResponse
from sqlalchemy.orm import Session
from ..config.database import get_db
from ..services.auth_service import auth_service
from ..schemas.auth import (
RegisterRequest,
LoginRequest,
RefreshTokenRequest,
ForgotPasswordRequest,
ResetPasswordRequest,
AuthResponse,
TokenResponse,
MessageResponse
)
from ..middleware.auth import get_current_user
from ..models.user import User
router = APIRouter(prefix="/auth", tags=["auth"])
@router.post("/register", status_code=status.HTTP_201_CREATED)
async def register(
request: RegisterRequest,
response: Response,
db: Session = Depends(get_db)
):
"""Register new user"""
try:
result = await auth_service.register(
db=db,
name=request.name,
email=request.email,
password=request.password,
phone=request.phone
)
# Set refresh token as HttpOnly cookie
response.set_cookie(
key="refreshToken",
value=result["refreshToken"],
httponly=True,
secure=False, # Set to True in production with HTTPS
samesite="strict",
max_age=7 * 24 * 60 * 60, # 7 days
path="/"
)
# Format response to match frontend expectations
return {
"status": "success",
"message": "Registration successful",
"data": {
"token": result["token"],
"user": result["user"]
}
}
except ValueError as e:
error_message = str(e)
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={
"status": "error",
"message": error_message
}
)
@router.post("/login")
async def login(
request: LoginRequest,
response: Response,
db: Session = Depends(get_db)
):
"""Login user"""
try:
result = await auth_service.login(
db=db,
email=request.email,
password=request.password,
remember_me=request.rememberMe or False
)
# Set refresh token as HttpOnly cookie
max_age = 7 * 24 * 60 * 60 if request.rememberMe else 1 * 24 * 60 * 60
response.set_cookie(
key="refreshToken",
value=result["refreshToken"],
httponly=True,
secure=False, # Set to True in production with HTTPS
samesite="strict",
max_age=max_age,
path="/"
)
# Format response to match frontend expectations
return {
"status": "success",
"data": {
"token": result["token"],
"user": result["user"]
}
}
except ValueError as e:
error_message = str(e)
status_code = status.HTTP_401_UNAUTHORIZED if "Invalid email or password" in error_message else status.HTTP_400_BAD_REQUEST
return JSONResponse(
status_code=status_code,
content={
"status": "error",
"message": error_message
}
)
@router.post("/refresh-token", response_model=TokenResponse)
async def refresh_token(
refreshToken: str = Cookie(None),
db: Session = Depends(get_db)
):
"""Refresh access token"""
if not refreshToken:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Refresh token not found"
)
try:
result = await auth_service.refresh_access_token(db, refreshToken)
return result
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=str(e)
)
@router.post("/logout", response_model=MessageResponse)
async def logout(
response: Response,
refreshToken: str = Cookie(None),
db: Session = Depends(get_db)
):
"""Logout user"""
if refreshToken:
await auth_service.logout(db, refreshToken)
# Clear refresh token cookie
response.delete_cookie(key="refreshToken", path="/")
return {
"status": "success",
"message": "Logout successful"
}
@router.get("/profile")
async def get_profile(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Get current user profile"""
try:
user = await auth_service.get_profile(db, current_user.id)
return user
except ValueError as e:
if "User not found" in str(e):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=str(e)
)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
@router.post("/forgot-password", response_model=MessageResponse)
async def forgot_password(
request: ForgotPasswordRequest,
db: Session = Depends(get_db)
):
"""Send password reset link"""
result = await auth_service.forgot_password(db, request.email)
return {
"status": "success",
"message": result["message"]
}
@router.post("/reset-password", response_model=MessageResponse)
async def reset_password(
request: ResetPasswordRequest,
db: Session = Depends(get_db)
):
"""Reset password with token"""
try:
result = await auth_service.reset_password(
db=db,
token=request.token,
password=request.password
)
return {
"status": "success",
"message": result["message"]
}
except ValueError as e:
status_code = status.HTTP_400_BAD_REQUEST
if "User not found" in str(e):
status_code = status.HTTP_404_NOT_FOUND
raise HTTPException(
status_code=status_code,
detail=str(e)
)

View File

@@ -0,0 +1,229 @@
from fastapi import APIRouter, Depends, HTTPException, status, Query, Request
from sqlalchemy.orm import Session
from sqlalchemy import and_, or_
from typing import Optional
from datetime import datetime
import os
from ..config.database import get_db
from ..middleware.auth import get_current_user, authorize_roles
from ..models.user import User
from ..models.banner import Banner
router = APIRouter(prefix="/banners", tags=["banners"])
def normalize_image_url(image_url: str, base_url: str) -> str:
"""Normalize image URL to absolute URL"""
if not image_url:
return image_url
if image_url.startswith('http://') or image_url.startswith('https://'):
return image_url
if image_url.startswith('/'):
return f"{base_url}{image_url}"
return f"{base_url}/{image_url}"
def get_base_url(request: Request) -> str:
"""Get base URL for image normalization"""
return os.getenv("SERVER_URL") or f"http://{request.headers.get('host', 'localhost:3000')}"
@router.get("/")
async def get_banners(
request: Request,
position: Optional[str] = Query(None),
db: Session = Depends(get_db)
):
"""Get all active banners"""
try:
query = db.query(Banner).filter(Banner.is_active == True)
# Filter by position
if position:
query = query.filter(Banner.position == position)
# Filter by date range
now = datetime.utcnow()
query = query.filter(
or_(
Banner.start_date == None,
Banner.start_date <= now
)
).filter(
or_(
Banner.end_date == None,
Banner.end_date >= now
)
)
banners = query.order_by(Banner.display_order.asc(), Banner.created_at.desc()).all()
base_url = get_base_url(request)
result = []
for banner in banners:
banner_dict = {
"id": banner.id,
"title": banner.title,
"description": banner.description,
"image_url": normalize_image_url(banner.image_url, base_url),
"link_url": banner.link_url,
"position": banner.position,
"display_order": banner.display_order,
"is_active": banner.is_active,
"start_date": banner.start_date.isoformat() if banner.start_date else None,
"end_date": banner.end_date.isoformat() if banner.end_date else None,
"created_at": banner.created_at.isoformat() if banner.created_at else None,
}
result.append(banner_dict)
return {
"status": "success",
"data": {"banners": result}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/{id}")
async def get_banner_by_id(
id: int,
request: Request,
db: Session = Depends(get_db)
):
"""Get banner by ID"""
try:
banner = db.query(Banner).filter(Banner.id == id).first()
if not banner:
raise HTTPException(status_code=404, detail="Banner not found")
base_url = get_base_url(request)
banner_dict = {
"id": banner.id,
"title": banner.title,
"description": banner.description,
"image_url": normalize_image_url(banner.image_url, base_url),
"link_url": banner.link_url,
"position": banner.position,
"display_order": banner.display_order,
"is_active": banner.is_active,
"start_date": banner.start_date.isoformat() if banner.start_date else None,
"end_date": banner.end_date.isoformat() if banner.end_date else None,
"created_at": banner.created_at.isoformat() if banner.created_at else None,
}
return {
"status": "success",
"data": {"banner": banner_dict}
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/", dependencies=[Depends(authorize_roles("admin"))])
async def create_banner(
banner_data: dict,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Create new banner (Admin only)"""
try:
banner = Banner(
title=banner_data.get("title"),
description=banner_data.get("description"),
image_url=banner_data.get("image_url"),
link_url=banner_data.get("link"),
position=banner_data.get("position", "home"),
display_order=banner_data.get("display_order", 0),
is_active=True,
start_date=datetime.fromisoformat(banner_data["start_date"].replace('Z', '+00:00')) if banner_data.get("start_date") else None,
end_date=datetime.fromisoformat(banner_data["end_date"].replace('Z', '+00:00')) if banner_data.get("end_date") else None,
)
db.add(banner)
db.commit()
db.refresh(banner)
return {
"status": "success",
"message": "Banner created successfully",
"data": {"banner": banner}
}
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.put("/{id}", dependencies=[Depends(authorize_roles("admin"))])
async def update_banner(
id: int,
banner_data: dict,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Update banner (Admin only)"""
try:
banner = db.query(Banner).filter(Banner.id == id).first()
if not banner:
raise HTTPException(status_code=404, detail="Banner not found")
if "title" in banner_data:
banner.title = banner_data["title"]
if "description" in banner_data:
banner.description = banner_data["description"]
if "image_url" in banner_data:
banner.image_url = banner_data["image_url"]
if "link" in banner_data:
banner.link_url = banner_data["link"]
if "position" in banner_data:
banner.position = banner_data["position"]
if "display_order" in banner_data:
banner.display_order = banner_data["display_order"]
if "is_active" in banner_data:
banner.is_active = banner_data["is_active"]
if "start_date" in banner_data:
banner.start_date = datetime.fromisoformat(banner_data["start_date"].replace('Z', '+00:00')) if banner_data["start_date"] else None
if "end_date" in banner_data:
banner.end_date = datetime.fromisoformat(banner_data["end_date"].replace('Z', '+00:00')) if banner_data["end_date"] else None
db.commit()
db.refresh(banner)
return {
"status": "success",
"message": "Banner updated successfully",
"data": {"banner": banner}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/{id}", dependencies=[Depends(authorize_roles("admin"))])
async def delete_banner(
id: int,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Delete banner (Admin only)"""
try:
banner = db.query(Banner).filter(Banner.id == id).first()
if not banner:
raise HTTPException(status_code=404, detail="Banner not found")
db.delete(banner)
db.commit()
return {
"status": "success",
"message": "Banner deleted successfully"
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -0,0 +1,437 @@
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from sqlalchemy import and_, or_
from typing import Optional
from datetime import datetime
import random
from ..config.database import get_db
from ..middleware.auth import get_current_user, authorize_roles
from ..models.user import User
from ..models.booking import Booking, BookingStatus
from ..models.room import Room
from ..models.room_type import RoomType
from ..models.payment import Payment, PaymentMethod, PaymentType, PaymentStatus
router = APIRouter(prefix="/bookings", tags=["bookings"])
def generate_booking_number() -> str:
"""Generate unique booking number"""
prefix = "BK"
ts = int(datetime.utcnow().timestamp() * 1000)
rand = random.randint(1000, 9999)
return f"{prefix}-{ts}-{rand}"
@router.get("/")
async def get_all_bookings(
search: Optional[str] = Query(None),
status_filter: Optional[str] = Query(None, alias="status"),
startDate: Optional[str] = Query(None),
endDate: Optional[str] = Query(None),
page: int = Query(1, ge=1),
limit: int = Query(10, ge=1, le=100),
current_user: User = Depends(authorize_roles("admin", "staff")),
db: Session = Depends(get_db)
):
"""Get all bookings (Admin/Staff only)"""
try:
query = db.query(Booking)
# Filter by search (booking_number)
if search:
query = query.filter(Booking.booking_number.like(f"%{search}%"))
# Filter by status
if status_filter:
try:
query = query.filter(Booking.status == BookingStatus(status_filter))
except ValueError:
pass
# Filter by date range
if startDate:
start = datetime.fromisoformat(startDate.replace('Z', '+00:00'))
query = query.filter(Booking.check_in_date >= start)
if endDate:
end = datetime.fromisoformat(endDate.replace('Z', '+00:00'))
query = query.filter(Booking.check_in_date <= end)
# Get total count
total = query.count()
# Apply pagination
offset = (page - 1) * limit
bookings = query.order_by(Booking.created_at.desc()).offset(offset).limit(limit).all()
# Include related data
result = []
for booking in bookings:
booking_dict = {
"id": booking.id,
"booking_number": booking.booking_number,
"user_id": booking.user_id,
"room_id": booking.room_id,
"check_in_date": booking.check_in_date.isoformat() if booking.check_in_date else None,
"check_out_date": booking.check_out_date.isoformat() if booking.check_out_date else None,
"num_guests": booking.num_guests,
"total_price": float(booking.total_price) if booking.total_price else 0.0,
"status": booking.status.value if isinstance(booking.status, BookingStatus) else booking.status,
"deposit_paid": booking.deposit_paid,
"requires_deposit": booking.requires_deposit,
"special_requests": booking.special_requests,
"created_at": booking.created_at.isoformat() if booking.created_at else None,
}
# Add user info
if booking.user:
booking_dict["user"] = {
"id": booking.user.id,
"full_name": booking.user.full_name,
"email": booking.user.email,
"phone": booking.user.phone,
}
# Add room info
if booking.room:
booking_dict["room"] = {
"id": booking.room.id,
"room_number": booking.room.room_number,
"floor": booking.room.floor,
}
result.append(booking_dict)
return {
"status": "success",
"data": {
"bookings": result,
"pagination": {
"total": total,
"page": page,
"limit": limit,
"totalPages": (total + limit - 1) // limit,
},
},
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/me")
async def get_my_bookings(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Get current user's bookings"""
try:
bookings = db.query(Booking).filter(
Booking.user_id == current_user.id
).order_by(Booking.created_at.desc()).all()
result = []
for booking in bookings:
booking_dict = {
"id": booking.id,
"booking_number": booking.booking_number,
"room_id": booking.room_id,
"check_in_date": booking.check_in_date.isoformat() if booking.check_in_date else None,
"check_out_date": booking.check_out_date.isoformat() if booking.check_out_date else None,
"num_guests": booking.num_guests,
"total_price": float(booking.total_price) if booking.total_price else 0.0,
"status": booking.status.value if isinstance(booking.status, BookingStatus) else booking.status,
"deposit_paid": booking.deposit_paid,
"requires_deposit": booking.requires_deposit,
"special_requests": booking.special_requests,
"created_at": booking.created_at.isoformat() if booking.created_at else None,
}
# Add room info
if booking.room and booking.room.room_type:
booking_dict["room"] = {
"id": booking.room.id,
"room_number": booking.room.room_number,
"room_type": {
"name": booking.room.room_type.name,
}
}
result.append(booking_dict)
return {
"success": True,
"data": {"bookings": result}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/")
async def create_booking(
booking_data: dict,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Create new booking"""
try:
room_id = booking_data.get("room_id")
check_in_date = booking_data.get("check_in_date")
check_out_date = booking_data.get("check_out_date")
total_price = booking_data.get("total_price")
guest_count = booking_data.get("guest_count", 1)
notes = booking_data.get("notes")
payment_method = booking_data.get("payment_method", "cash")
if not all([room_id, check_in_date, check_out_date, total_price]):
raise HTTPException(status_code=400, detail="Missing required booking fields")
# Check if room exists
room = db.query(Room).filter(Room.id == room_id).first()
if not room:
raise HTTPException(status_code=404, detail="Room not found")
check_in = datetime.fromisoformat(check_in_date.replace('Z', '+00:00'))
check_out = datetime.fromisoformat(check_out_date.replace('Z', '+00:00'))
# Check for overlapping bookings
overlapping = db.query(Booking).filter(
and_(
Booking.room_id == room_id,
Booking.status != BookingStatus.cancelled,
Booking.check_in_date < check_out,
Booking.check_out_date > check_in
)
).first()
if overlapping:
raise HTTPException(
status_code=409,
detail="Room already booked for the selected dates"
)
booking_number = generate_booking_number()
# Determine if deposit is required
requires_deposit = payment_method == "cash"
deposit_percentage = 20 if requires_deposit else 0
deposit_amount = (float(total_price) * deposit_percentage) / 100 if requires_deposit else 0
# Create booking
booking = Booking(
booking_number=booking_number,
user_id=current_user.id,
room_id=room_id,
check_in_date=check_in,
check_out_date=check_out,
num_guests=guest_count,
total_price=total_price,
special_requests=notes,
status=BookingStatus.pending,
requires_deposit=requires_deposit,
deposit_paid=False,
)
db.add(booking)
db.flush()
# Create deposit payment if required
if requires_deposit:
payment = Payment(
booking_id=booking.id,
amount=deposit_amount,
payment_method=PaymentMethod.bank_transfer,
payment_type=PaymentType.deposit,
deposit_percentage=deposit_percentage,
payment_status=PaymentStatus.pending,
notes=f"Deposit payment ({deposit_percentage}%) for booking {booking_number}",
)
db.add(payment)
db.commit()
db.refresh(booking)
# Fetch with relations
booking = db.query(Booking).filter(Booking.id == booking.id).first()
return {
"success": True,
"data": {"booking": booking},
"message": f"Booking created. Please pay {deposit_percentage}% deposit to confirm." if requires_deposit else "Booking created successfully"
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.get("/{id}")
async def get_booking_by_id(
id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Get booking by ID"""
try:
booking = db.query(Booking).filter(Booking.id == id).first()
if not booking:
raise HTTPException(status_code=404, detail="Booking not found")
# Check access
if current_user.role_id != 1 and booking.user_id != current_user.id: # Not admin
raise HTTPException(status_code=403, detail="Forbidden")
booking_dict = {
"id": booking.id,
"booking_number": booking.booking_number,
"user_id": booking.user_id,
"room_id": booking.room_id,
"check_in_date": booking.check_in_date.isoformat() if booking.check_in_date else None,
"check_out_date": booking.check_out_date.isoformat() if booking.check_out_date else None,
"num_guests": booking.num_guests,
"total_price": float(booking.total_price) if booking.total_price else 0.0,
"status": booking.status.value if isinstance(booking.status, BookingStatus) else booking.status,
"deposit_paid": booking.deposit_paid,
"requires_deposit": booking.requires_deposit,
"special_requests": booking.special_requests,
"created_at": booking.created_at.isoformat() if booking.created_at else None,
}
# Add relations
if booking.room:
booking_dict["room"] = {
"id": booking.room.id,
"room_number": booking.room.room_number,
}
if booking.room.room_type:
booking_dict["room"]["room_type"] = {
"name": booking.room.room_type.name,
}
if booking.payments:
booking_dict["payments"] = [
{
"id": p.id,
"amount": float(p.amount) if p.amount else 0.0,
"payment_method": p.payment_method.value if isinstance(p.payment_method, PaymentMethod) else p.payment_method,
"payment_status": p.payment_status.value if isinstance(p.payment_status, PaymentStatus) else p.payment_status,
}
for p in booking.payments
]
return {
"success": True,
"data": {"booking": booking_dict}
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.patch("/{id}/cancel")
async def cancel_booking(
id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Cancel a booking"""
try:
booking = db.query(Booking).filter(Booking.id == id).first()
if not booking:
raise HTTPException(status_code=404, detail="Booking not found")
if booking.user_id != current_user.id:
raise HTTPException(status_code=403, detail="Forbidden")
if booking.status == BookingStatus.cancelled:
raise HTTPException(status_code=400, detail="Booking already cancelled")
booking.status = BookingStatus.cancelled
db.commit()
return {
"success": True,
"data": {"booking": booking}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.put("/{id}", dependencies=[Depends(authorize_roles("admin"))])
async def update_booking(
id: int,
booking_data: dict,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Update booking status (Admin only)"""
try:
booking = db.query(Booking).filter(Booking.id == id).first()
if not booking:
raise HTTPException(status_code=404, detail="Booking not found")
status_value = booking_data.get("status")
if status_value:
try:
booking.status = BookingStatus(status_value)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid status")
db.commit()
db.refresh(booking)
return {
"status": "success",
"message": "Booking updated successfully",
"data": {"booking": booking}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.get("/check/{booking_number}")
async def check_booking_by_number(
booking_number: str,
db: Session = Depends(get_db)
):
"""Check booking by booking number"""
try:
booking = db.query(Booking).filter(Booking.booking_number == booking_number).first()
if not booking:
raise HTTPException(status_code=404, detail="Booking not found")
booking_dict = {
"id": booking.id,
"booking_number": booking.booking_number,
"room_id": booking.room_id,
"check_in_date": booking.check_in_date.isoformat() if booking.check_in_date else None,
"check_out_date": booking.check_out_date.isoformat() if booking.check_out_date else None,
"status": booking.status.value if isinstance(booking.status, BookingStatus) else booking.status,
}
if booking.room:
booking_dict["room"] = {
"id": booking.room.id,
"room_number": booking.room.room_number,
}
return {
"status": "success",
"data": {"booking": booking_dict}
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -0,0 +1,187 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from sqlalchemy import func
from ..config.database import get_db
from ..middleware.auth import get_current_user
from ..models.user import User
from ..models.favorite import Favorite
from ..models.room import Room
from ..models.room_type import RoomType
from ..models.review import Review, ReviewStatus
router = APIRouter(prefix="/favorites", tags=["favorites"])
@router.get("/")
async def get_favorites(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Get user's favorite rooms"""
try:
favorites = db.query(Favorite).filter(
Favorite.user_id == current_user.id
).order_by(Favorite.created_at.desc()).all()
result = []
for favorite in favorites:
if not favorite.room:
continue
room = favorite.room
# Get review stats
review_stats = db.query(
func.avg(Review.rating).label('average_rating'),
func.count(Review.id).label('total_reviews')
).filter(
Review.room_id == room.id,
Review.status == ReviewStatus.approved
).first()
room_dict = {
"id": room.id,
"room_type_id": room.room_type_id,
"room_number": room.room_number,
"floor": room.floor,
"status": room.status.value if hasattr(room.status, 'value') else room.status,
"price": float(room.price) if room.price else 0.0,
"featured": room.featured,
"description": room.description,
"amenities": room.amenities,
"images": room.images or [],
"average_rating": round(float(review_stats.average_rating or 0), 1) if review_stats and review_stats.average_rating else None,
"total_reviews": review_stats.total_reviews or 0 if review_stats else 0,
}
if room.room_type:
room_dict["room_type"] = {
"id": room.room_type.id,
"name": room.room_type.name,
"description": room.room_type.description,
"base_price": float(room.room_type.base_price) if room.room_type.base_price else 0.0,
"capacity": room.room_type.capacity,
"amenities": room.room_type.amenities,
}
favorite_dict = {
"id": favorite.id,
"user_id": favorite.user_id,
"room_id": favorite.room_id,
"room": room_dict,
"created_at": favorite.created_at.isoformat() if favorite.created_at else None,
}
result.append(favorite_dict)
return {
"status": "success",
"data": {
"favorites": result,
"total": len(result),
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/{room_id}")
async def add_favorite(
room_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Add room to favorites"""
try:
# Check if room exists
room = db.query(Room).filter(Room.id == room_id).first()
if not room:
raise HTTPException(status_code=404, detail="Room not found")
# Check if already favorited
existing = db.query(Favorite).filter(
Favorite.user_id == current_user.id,
Favorite.room_id == room_id
).first()
if existing:
raise HTTPException(
status_code=400,
detail="Room already in favorites list"
)
# Create favorite
favorite = Favorite(
user_id=current_user.id,
room_id=room_id
)
db.add(favorite)
db.commit()
db.refresh(favorite)
return {
"status": "success",
"message": "Added to favorites list",
"data": {"favorite": favorite}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/{room_id}")
async def remove_favorite(
room_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Remove room from favorites"""
try:
favorite = db.query(Favorite).filter(
Favorite.user_id == current_user.id,
Favorite.room_id == room_id
).first()
if not favorite:
raise HTTPException(
status_code=404,
detail="Room not found in favorites list"
)
db.delete(favorite)
db.commit()
return {
"status": "success",
"message": "Removed from favorites list"
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.get("/check/{room_id}")
async def check_favorite(
room_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Check if room is favorited by user"""
try:
favorite = db.query(Favorite).filter(
Favorite.user_id == current_user.id,
Favorite.room_id == room_id
).first()
return {
"status": "success",
"data": {"isFavorited": favorite is not None}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -0,0 +1,228 @@
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from typing import Optional
from datetime import datetime
from ..config.database import get_db
from ..middleware.auth import get_current_user, authorize_roles
from ..models.user import User
from ..models.payment import Payment, PaymentMethod, PaymentType, PaymentStatus
from ..models.booking import Booking
router = APIRouter(prefix="/payments", tags=["payments"])
@router.get("/")
async def get_payments(
booking_id: Optional[int] = Query(None),
status_filter: Optional[str] = Query(None, alias="status"),
page: int = Query(1, ge=1),
limit: int = Query(10, ge=1, le=100),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Get all payments"""
try:
query = db.query(Payment)
# Filter by booking_id
if booking_id:
query = query.filter(Payment.booking_id == booking_id)
# Filter by status
if status_filter:
try:
query = query.filter(Payment.payment_status == PaymentStatus(status_filter))
except ValueError:
pass
# Users can only see their own payments unless admin
if current_user.role_id != 1: # Not admin
query = query.join(Booking).filter(Booking.user_id == current_user.id)
total = query.count()
offset = (page - 1) * limit
payments = query.order_by(Payment.created_at.desc()).offset(offset).limit(limit).all()
result = []
for payment in payments:
payment_dict = {
"id": payment.id,
"booking_id": payment.booking_id,
"amount": float(payment.amount) if payment.amount else 0.0,
"payment_method": payment.payment_method.value if isinstance(payment.payment_method, PaymentMethod) else payment.payment_method,
"payment_type": payment.payment_type.value if isinstance(payment.payment_type, PaymentType) else payment.payment_type,
"deposit_percentage": payment.deposit_percentage,
"related_payment_id": payment.related_payment_id,
"payment_status": payment.payment_status.value if isinstance(payment.payment_status, PaymentStatus) else payment.payment_status,
"transaction_id": payment.transaction_id,
"payment_date": payment.payment_date.isoformat() if payment.payment_date else None,
"notes": payment.notes,
"created_at": payment.created_at.isoformat() if payment.created_at else None,
}
if payment.booking:
payment_dict["booking"] = {
"id": payment.booking.id,
"booking_number": payment.booking.booking_number,
}
result.append(payment_dict)
return {
"status": "success",
"data": {
"payments": result,
"pagination": {
"total": total,
"page": page,
"limit": limit,
"totalPages": (total + limit - 1) // limit,
},
},
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/{id}")
async def get_payment_by_id(
id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Get payment by ID"""
try:
payment = db.query(Payment).filter(Payment.id == id).first()
if not payment:
raise HTTPException(status_code=404, detail="Payment not found")
# Check access
if current_user.role_id != 1: # Not admin
if payment.booking and payment.booking.user_id != current_user.id:
raise HTTPException(status_code=403, detail="Forbidden")
payment_dict = {
"id": payment.id,
"booking_id": payment.booking_id,
"amount": float(payment.amount) if payment.amount else 0.0,
"payment_method": payment.payment_method.value if isinstance(payment.payment_method, PaymentMethod) else payment.payment_method,
"payment_type": payment.payment_type.value if isinstance(payment.payment_type, PaymentType) else payment.payment_type,
"deposit_percentage": payment.deposit_percentage,
"related_payment_id": payment.related_payment_id,
"payment_status": payment.payment_status.value if isinstance(payment.payment_status, PaymentStatus) else payment.payment_status,
"transaction_id": payment.transaction_id,
"payment_date": payment.payment_date.isoformat() if payment.payment_date else None,
"notes": payment.notes,
"created_at": payment.created_at.isoformat() if payment.created_at else None,
}
if payment.booking:
payment_dict["booking"] = {
"id": payment.booking.id,
"booking_number": payment.booking.booking_number,
}
return {
"status": "success",
"data": {"payment": payment_dict}
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/")
async def create_payment(
payment_data: dict,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Create new payment"""
try:
booking_id = payment_data.get("booking_id")
amount = float(payment_data.get("amount", 0))
payment_method = payment_data.get("payment_method", "cash")
payment_type = payment_data.get("payment_type", "full")
# Check if booking exists
booking = db.query(Booking).filter(Booking.id == booking_id).first()
if not booking:
raise HTTPException(status_code=404, detail="Booking not found")
# Check access
if current_user.role_id != 1 and booking.user_id != current_user.id:
raise HTTPException(status_code=403, detail="Forbidden")
# Create payment
payment = Payment(
booking_id=booking_id,
amount=amount,
payment_method=PaymentMethod(payment_method),
payment_type=PaymentType(payment_type),
payment_status=PaymentStatus.pending,
payment_date=datetime.utcnow() if payment_data.get("mark_as_paid") else None,
notes=payment_data.get("notes"),
)
# If marked as paid, update status
if payment_data.get("mark_as_paid"):
payment.payment_status = PaymentStatus.completed
db.add(payment)
db.commit()
db.refresh(payment)
return {
"status": "success",
"message": "Payment created successfully",
"data": {"payment": payment}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.put("/{id}/status", dependencies=[Depends(authorize_roles("admin", "staff"))])
async def update_payment_status(
id: int,
status_data: dict,
current_user: User = Depends(authorize_roles("admin", "staff")),
db: Session = Depends(get_db)
):
"""Update payment status (Admin/Staff only)"""
try:
payment = db.query(Payment).filter(Payment.id == id).first()
if not payment:
raise HTTPException(status_code=404, detail="Payment not found")
status_value = status_data.get("status")
if status_value:
try:
payment.payment_status = PaymentStatus(status_value)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid payment status")
if status_data.get("transaction_id"):
payment.transaction_id = status_data["transaction_id"]
if status_data.get("mark_as_paid"):
payment.payment_status = PaymentStatus.completed
payment.payment_date = datetime.utcnow()
db.commit()
db.refresh(payment)
return {
"status": "success",
"message": "Payment status updated successfully",
"data": {"payment": payment}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -0,0 +1,334 @@
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from sqlalchemy import or_
from typing import Optional
from datetime import datetime
from ..config.database import get_db
from ..middleware.auth import get_current_user, authorize_roles
from ..models.user import User
from ..models.promotion import Promotion, DiscountType
router = APIRouter(prefix="/promotions", tags=["promotions"])
@router.get("/")
async def get_promotions(
search: Optional[str] = Query(None),
status_filter: Optional[str] = Query(None, alias="status"),
type: Optional[str] = Query(None),
page: int = Query(1, ge=1),
limit: int = Query(10, ge=1, le=100),
db: Session = Depends(get_db)
):
"""Get all promotions with filters"""
try:
query = db.query(Promotion)
# Filter by search (code or name)
if search:
query = query.filter(
or_(
Promotion.code.like(f"%{search}%"),
Promotion.name.like(f"%{search}%")
)
)
# Filter by status (is_active)
if status_filter:
is_active = status_filter == "active"
query = query.filter(Promotion.is_active == is_active)
# Filter by discount type
if type:
try:
query = query.filter(Promotion.discount_type == DiscountType(type))
except ValueError:
pass
total = query.count()
offset = (page - 1) * limit
promotions = query.order_by(Promotion.created_at.desc()).offset(offset).limit(limit).all()
result = []
for promo in promotions:
promo_dict = {
"id": promo.id,
"code": promo.code,
"name": promo.name,
"description": promo.description,
"discount_type": promo.discount_type.value if isinstance(promo.discount_type, DiscountType) else promo.discount_type,
"discount_value": float(promo.discount_value) if promo.discount_value else 0.0,
"min_booking_amount": float(promo.min_booking_amount) if promo.min_booking_amount else None,
"max_discount_amount": float(promo.max_discount_amount) if promo.max_discount_amount else None,
"start_date": promo.start_date.isoformat() if promo.start_date else None,
"end_date": promo.end_date.isoformat() if promo.end_date else None,
"usage_limit": promo.usage_limit,
"used_count": promo.used_count,
"is_active": promo.is_active,
"created_at": promo.created_at.isoformat() if promo.created_at else None,
}
result.append(promo_dict)
return {
"status": "success",
"data": {
"promotions": result,
"pagination": {
"total": total,
"page": page,
"limit": limit,
"totalPages": (total + limit - 1) // limit,
},
},
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/{code}")
async def get_promotion_by_code(code: str, db: Session = Depends(get_db)):
"""Get promotion by code"""
try:
promotion = db.query(Promotion).filter(Promotion.code == code).first()
if not promotion:
raise HTTPException(status_code=404, detail="Promotion not found")
promo_dict = {
"id": promotion.id,
"code": promotion.code,
"name": promotion.name,
"description": promotion.description,
"discount_type": promotion.discount_type.value if isinstance(promotion.discount_type, DiscountType) else promotion.discount_type,
"discount_value": float(promotion.discount_value) if promotion.discount_value else 0.0,
"min_booking_amount": float(promotion.min_booking_amount) if promotion.min_booking_amount else None,
"max_discount_amount": float(promotion.max_discount_amount) if promotion.max_discount_amount else None,
"start_date": promotion.start_date.isoformat() if promotion.start_date else None,
"end_date": promotion.end_date.isoformat() if promotion.end_date else None,
"usage_limit": promotion.usage_limit,
"used_count": promotion.used_count,
"is_active": promotion.is_active,
}
return {
"status": "success",
"data": {"promotion": promo_dict}
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/validate")
async def validate_promotion(
validation_data: dict,
db: Session = Depends(get_db)
):
"""Validate and apply promotion"""
try:
code = validation_data.get("code")
booking_amount = float(validation_data.get("booking_amount", 0))
promotion = db.query(Promotion).filter(Promotion.code == code).first()
if not promotion:
raise HTTPException(status_code=404, detail="Promotion code not found")
# Check if promotion is active
if not promotion.is_active:
raise HTTPException(status_code=400, detail="Promotion is not active")
# Check date validity
now = datetime.utcnow()
if promotion.start_date and now < promotion.start_date:
raise HTTPException(status_code=400, detail="Promotion is not valid at this time")
if promotion.end_date and now > promotion.end_date:
raise HTTPException(status_code=400, detail="Promotion is not valid at this time")
# Check usage limit
if promotion.usage_limit and promotion.used_count >= promotion.usage_limit:
raise HTTPException(status_code=400, detail="Promotion usage limit reached")
# Check minimum booking amount
if promotion.min_booking_amount and booking_amount < float(promotion.min_booking_amount):
raise HTTPException(
status_code=400,
detail=f"Minimum booking amount is {promotion.min_booking_amount}"
)
# Calculate discount
discount_amount = promotion.calculate_discount(booking_amount)
final_amount = booking_amount - discount_amount
return {
"status": "success",
"data": {
"promotion": {
"id": promotion.id,
"code": promotion.code,
"name": promotion.name,
},
"original_amount": booking_amount,
"discount_amount": discount_amount,
"final_amount": final_amount,
}
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/", dependencies=[Depends(authorize_roles("admin"))])
async def create_promotion(
promotion_data: dict,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Create new promotion (Admin only)"""
try:
code = promotion_data.get("code")
# Check if code exists
existing = db.query(Promotion).filter(Promotion.code == code).first()
if existing:
raise HTTPException(status_code=400, detail="Promotion code already exists")
discount_type = promotion_data.get("discount_type")
discount_value = float(promotion_data.get("discount_value", 0))
# Validate discount value
if discount_type == "percentage" and discount_value > 100:
raise HTTPException(
status_code=400,
detail="Percentage discount cannot exceed 100%"
)
promotion = Promotion(
code=code,
name=promotion_data.get("name"),
description=promotion_data.get("description"),
discount_type=DiscountType(discount_type),
discount_value=discount_value,
min_booking_amount=float(promotion_data["min_booking_amount"]) if promotion_data.get("min_booking_amount") else None,
max_discount_amount=float(promotion_data["max_discount_amount"]) if promotion_data.get("max_discount_amount") else None,
start_date=datetime.fromisoformat(promotion_data["start_date"].replace('Z', '+00:00')) if promotion_data.get("start_date") else None,
end_date=datetime.fromisoformat(promotion_data["end_date"].replace('Z', '+00:00')) if promotion_data.get("end_date") else None,
usage_limit=promotion_data.get("usage_limit"),
used_count=0,
is_active=promotion_data.get("status") == "active" if promotion_data.get("status") else True,
)
db.add(promotion)
db.commit()
db.refresh(promotion)
return {
"status": "success",
"message": "Promotion created successfully",
"data": {"promotion": promotion}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.put("/{id}", dependencies=[Depends(authorize_roles("admin"))])
async def update_promotion(
id: int,
promotion_data: dict,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Update promotion (Admin only)"""
try:
promotion = db.query(Promotion).filter(Promotion.id == id).first()
if not promotion:
raise HTTPException(status_code=404, detail="Promotion not found")
# Check if new code exists (excluding current)
code = promotion_data.get("code")
if code and code != promotion.code:
existing = db.query(Promotion).filter(
Promotion.code == code,
Promotion.id != id
).first()
if existing:
raise HTTPException(status_code=400, detail="Promotion code already exists")
# Validate discount value
discount_type = promotion_data.get("discount_type", promotion.discount_type.value if isinstance(promotion.discount_type, DiscountType) else promotion.discount_type)
discount_value = promotion_data.get("discount_value")
if discount_value is not None:
discount_value = float(discount_value)
if discount_type == "percentage" and discount_value > 100:
raise HTTPException(
status_code=400,
detail="Percentage discount cannot exceed 100%"
)
# Update fields
if "code" in promotion_data:
promotion.code = promotion_data["code"]
if "name" in promotion_data:
promotion.name = promotion_data["name"]
if "description" in promotion_data:
promotion.description = promotion_data["description"]
if "discount_type" in promotion_data:
promotion.discount_type = DiscountType(promotion_data["discount_type"])
if "discount_value" in promotion_data:
promotion.discount_value = discount_value
if "min_booking_amount" in promotion_data:
promotion.min_booking_amount = float(promotion_data["min_booking_amount"]) if promotion_data["min_booking_amount"] else None
if "max_discount_amount" in promotion_data:
promotion.max_discount_amount = float(promotion_data["max_discount_amount"]) if promotion_data["max_discount_amount"] else None
if "start_date" in promotion_data:
promotion.start_date = datetime.fromisoformat(promotion_data["start_date"].replace('Z', '+00:00')) if promotion_data["start_date"] else None
if "end_date" in promotion_data:
promotion.end_date = datetime.fromisoformat(promotion_data["end_date"].replace('Z', '+00:00')) if promotion_data["end_date"] else None
if "usage_limit" in promotion_data:
promotion.usage_limit = promotion_data["usage_limit"]
if "status" in promotion_data:
promotion.is_active = promotion_data["status"] == "active"
db.commit()
db.refresh(promotion)
return {
"status": "success",
"message": "Promotion updated successfully",
"data": {"promotion": promotion}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/{id}", dependencies=[Depends(authorize_roles("admin"))])
async def delete_promotion(
id: int,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Delete promotion (Admin only)"""
try:
promotion = db.query(Promotion).filter(Promotion.id == id).first()
if not promotion:
raise HTTPException(status_code=404, detail="Promotion not found")
db.delete(promotion)
db.commit()
return {
"status": "success",
"message": "Promotion deleted successfully"
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -0,0 +1,288 @@
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from sqlalchemy import func, and_
from typing import Optional
from datetime import datetime, timedelta
from ..config.database import get_db
from ..middleware.auth import get_current_user, authorize_roles
from ..models.user import User
from ..models.booking import Booking, BookingStatus
from ..models.payment import Payment, PaymentStatus
from ..models.room import Room
router = APIRouter(prefix="/reports", tags=["reports"])
@router.get("")
async def get_reports(
from_date: Optional[str] = Query(None, alias="from"),
to_date: Optional[str] = Query(None, alias="to"),
type: Optional[str] = Query(None),
current_user: User = Depends(authorize_roles("admin", "staff")),
db: Session = Depends(get_db)
):
"""Get comprehensive reports (Admin/Staff only)"""
try:
# Parse dates if provided
start_date = None
end_date = None
if from_date:
try:
start_date = datetime.strptime(from_date, "%Y-%m-%d")
except ValueError:
start_date = datetime.fromisoformat(from_date.replace('Z', '+00:00'))
if to_date:
try:
end_date = datetime.strptime(to_date, "%Y-%m-%d")
# Set to end of day
end_date = end_date.replace(hour=23, minute=59, second=59)
except ValueError:
end_date = datetime.fromisoformat(to_date.replace('Z', '+00:00'))
# Base queries
booking_query = db.query(Booking)
payment_query = db.query(Payment).filter(Payment.payment_status == PaymentStatus.completed)
# Apply date filters
if start_date:
booking_query = booking_query.filter(Booking.created_at >= start_date)
payment_query = payment_query.filter(Payment.payment_date >= start_date)
if end_date:
booking_query = booking_query.filter(Booking.created_at <= end_date)
payment_query = payment_query.filter(Payment.payment_date <= end_date)
# Total bookings
total_bookings = booking_query.count()
# Total revenue
total_revenue = payment_query.with_entities(func.sum(Payment.amount)).scalar() or 0.0
# Total customers (unique users with bookings)
total_customers = db.query(func.count(func.distinct(Booking.user_id))).scalar() or 0
if start_date or end_date:
customer_query = db.query(func.count(func.distinct(Booking.user_id)))
if start_date:
customer_query = customer_query.filter(Booking.created_at >= start_date)
if end_date:
customer_query = customer_query.filter(Booking.created_at <= end_date)
total_customers = customer_query.scalar() or 0
# Available rooms
available_rooms = db.query(Room).filter(Room.status == "available").count()
# Occupied rooms (rooms with active bookings)
occupied_rooms = db.query(func.count(func.distinct(Booking.room_id))).filter(
Booking.status.in_([BookingStatus.confirmed, BookingStatus.checked_in])
).scalar() or 0
# Revenue by date (daily breakdown)
revenue_by_date = []
if start_date and end_date:
daily_revenue_query = db.query(
func.date(Payment.payment_date).label('date'),
func.sum(Payment.amount).label('revenue'),
func.count(func.distinct(Payment.booking_id)).label('bookings')
).filter(Payment.payment_status == PaymentStatus.completed)
if start_date:
daily_revenue_query = daily_revenue_query.filter(Payment.payment_date >= start_date)
if end_date:
daily_revenue_query = daily_revenue_query.filter(Payment.payment_date <= end_date)
daily_revenue_query = daily_revenue_query.group_by(
func.date(Payment.payment_date)
).order_by(func.date(Payment.payment_date))
daily_data = daily_revenue_query.all()
revenue_by_date = [
{
"date": str(date),
"revenue": float(revenue or 0),
"bookings": int(bookings or 0)
}
for date, revenue, bookings in daily_data
]
# Bookings by status
bookings_by_status = {}
for status in BookingStatus:
count = booking_query.filter(Booking.status == status).count()
status_name = status.value if hasattr(status, 'value') else str(status)
bookings_by_status[status_name] = count
# Top rooms (by revenue)
top_rooms_query = db.query(
Room.id,
Room.room_number,
func.count(Booking.id).label('bookings'),
func.sum(Payment.amount).label('revenue')
).join(Booking, Room.id == Booking.room_id).join(
Payment, Booking.id == Payment.booking_id
).filter(Payment.payment_status == PaymentStatus.completed)
if start_date:
top_rooms_query = top_rooms_query.filter(Booking.created_at >= start_date)
if end_date:
top_rooms_query = top_rooms_query.filter(Booking.created_at <= end_date)
top_rooms_data = top_rooms_query.group_by(Room.id, Room.room_number).order_by(
func.sum(Payment.amount).desc()
).limit(10).all()
top_rooms = [
{
"room_id": room_id,
"room_number": room_number,
"bookings": int(bookings or 0),
"revenue": float(revenue or 0)
}
for room_id, room_number, bookings, revenue in top_rooms_data
]
return {
"status": "success",
"success": True,
"data": {
"total_bookings": total_bookings,
"total_revenue": float(total_revenue),
"total_customers": int(total_customers),
"available_rooms": available_rooms,
"occupied_rooms": occupied_rooms,
"revenue_by_date": revenue_by_date if revenue_by_date else None,
"bookings_by_status": bookings_by_status,
"top_rooms": top_rooms if top_rooms else None,
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/dashboard")
async def get_dashboard_stats(
current_user: User = Depends(authorize_roles("admin", "staff")),
db: Session = Depends(get_db)
):
"""Get dashboard statistics (Admin/Staff only)"""
try:
# Total bookings
total_bookings = db.query(Booking).count()
# Active bookings
active_bookings = db.query(Booking).filter(
Booking.status.in_([BookingStatus.pending, BookingStatus.confirmed, BookingStatus.checked_in])
).count()
# Total revenue (from completed payments)
total_revenue = db.query(func.sum(Payment.amount)).filter(
Payment.payment_status == PaymentStatus.completed
).scalar() or 0.0
# Today's revenue
today_start = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
today_revenue = db.query(func.sum(Payment.amount)).filter(
and_(
Payment.payment_status == PaymentStatus.completed,
Payment.payment_date >= today_start
)
).scalar() or 0.0
# Total rooms
total_rooms = db.query(Room).count()
# Available rooms
available_rooms = db.query(Room).filter(Room.status == "available").count()
# Recent bookings (last 7 days)
week_ago = datetime.utcnow() - timedelta(days=7)
recent_bookings = db.query(Booking).filter(
Booking.created_at >= week_ago
).count()
# Pending payments
pending_payments = db.query(Payment).filter(
Payment.payment_status == PaymentStatus.pending
).count()
return {
"status": "success",
"data": {
"total_bookings": total_bookings,
"active_bookings": active_bookings,
"total_revenue": float(total_revenue),
"today_revenue": float(today_revenue),
"total_rooms": total_rooms,
"available_rooms": available_rooms,
"recent_bookings": recent_bookings,
"pending_payments": pending_payments,
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/revenue")
async def get_revenue_report(
start_date: Optional[str] = Query(None),
end_date: Optional[str] = Query(None),
current_user: User = Depends(authorize_roles("admin", "staff")),
db: Session = Depends(get_db)
):
"""Get revenue report (Admin/Staff only)"""
try:
query = db.query(Payment).filter(
Payment.payment_status == PaymentStatus.completed
)
if start_date:
start = datetime.fromisoformat(start_date.replace('Z', '+00:00'))
query = query.filter(Payment.payment_date >= start)
if end_date:
end = datetime.fromisoformat(end_date.replace('Z', '+00:00'))
query = query.filter(Payment.payment_date <= end)
# Total revenue
total_revenue = db.query(func.sum(Payment.amount)).filter(
Payment.payment_status == PaymentStatus.completed
).scalar() or 0.0
# Revenue by payment method
revenue_by_method = db.query(
Payment.payment_method,
func.sum(Payment.amount).label('total')
).filter(
Payment.payment_status == PaymentStatus.completed
).group_by(Payment.payment_method).all()
method_breakdown = {}
for method, total in revenue_by_method:
method_name = method.value if hasattr(method, 'value') else str(method)
method_breakdown[method_name] = float(total or 0)
# Revenue by date (daily breakdown)
daily_revenue = db.query(
func.date(Payment.payment_date).label('date'),
func.sum(Payment.amount).label('total')
).filter(
Payment.payment_status == PaymentStatus.completed
).group_by(func.date(Payment.payment_date)).order_by(func.date(Payment.payment_date).desc()).limit(30).all()
daily_breakdown = [
{
"date": date.isoformat() if isinstance(date, datetime) else str(date),
"revenue": float(total or 0)
}
for date, total in daily_revenue
]
return {
"status": "success",
"data": {
"total_revenue": float(total_revenue),
"revenue_by_method": method_breakdown,
"daily_breakdown": daily_breakdown,
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -0,0 +1,251 @@
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from typing import Optional
from ..config.database import get_db
from ..middleware.auth import get_current_user, authorize_roles
from ..models.user import User
from ..models.review import Review, ReviewStatus
from ..models.room import Room
router = APIRouter(prefix="/reviews", tags=["reviews"])
@router.get("/room/{room_id}")
async def get_room_reviews(room_id: int, db: Session = Depends(get_db)):
"""Get reviews for a room"""
try:
reviews = db.query(Review).filter(
Review.room_id == room_id,
Review.status == ReviewStatus.approved
).order_by(Review.created_at.desc()).all()
result = []
for review in reviews:
review_dict = {
"id": review.id,
"user_id": review.user_id,
"room_id": review.room_id,
"rating": review.rating,
"comment": review.comment,
"status": review.status.value if isinstance(review.status, ReviewStatus) else review.status,
"created_at": review.created_at.isoformat() if review.created_at else None,
}
if review.user:
review_dict["user"] = {
"id": review.user.id,
"full_name": review.user.full_name,
"email": review.user.email,
}
result.append(review_dict)
return {
"status": "success",
"data": {"reviews": result}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/", dependencies=[Depends(authorize_roles("admin"))])
async def get_all_reviews(
status_filter: Optional[str] = Query(None, alias="status"),
page: int = Query(1, ge=1),
limit: int = Query(10, ge=1, le=100),
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Get all reviews (Admin only)"""
try:
query = db.query(Review)
if status_filter:
try:
query = query.filter(Review.status == ReviewStatus(status_filter))
except ValueError:
pass
total = query.count()
offset = (page - 1) * limit
reviews = query.order_by(Review.created_at.desc()).offset(offset).limit(limit).all()
result = []
for review in reviews:
review_dict = {
"id": review.id,
"user_id": review.user_id,
"room_id": review.room_id,
"rating": review.rating,
"comment": review.comment,
"status": review.status.value if isinstance(review.status, ReviewStatus) else review.status,
"created_at": review.created_at.isoformat() if review.created_at else None,
}
if review.user:
review_dict["user"] = {
"id": review.user.id,
"full_name": review.user.full_name,
"email": review.user.email,
"phone": review.user.phone,
}
if review.room:
review_dict["room"] = {
"id": review.room.id,
"room_number": review.room.room_number,
}
result.append(review_dict)
return {
"status": "success",
"data": {
"reviews": result,
"pagination": {
"total": total,
"page": page,
"limit": limit,
"totalPages": (total + limit - 1) // limit,
},
},
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/")
async def create_review(
review_data: dict,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Create new review"""
try:
room_id = review_data.get("room_id")
rating = review_data.get("rating")
comment = review_data.get("comment")
# Check if room exists
room = db.query(Room).filter(Room.id == room_id).first()
if not room:
raise HTTPException(status_code=404, detail="Room not found")
# Check if user already reviewed this room
existing = db.query(Review).filter(
Review.user_id == current_user.id,
Review.room_id == room_id
).first()
if existing:
raise HTTPException(
status_code=400,
detail="You have already reviewed this room"
)
# Create review
review = Review(
user_id=current_user.id,
room_id=room_id,
rating=rating,
comment=comment,
status=ReviewStatus.pending,
)
db.add(review)
db.commit()
db.refresh(review)
return {
"status": "success",
"message": "Review submitted successfully and is pending approval",
"data": {"review": review}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.put("/{id}/approve", dependencies=[Depends(authorize_roles("admin"))])
async def approve_review(
id: int,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Approve review (Admin only)"""
try:
review = db.query(Review).filter(Review.id == id).first()
if not review:
raise HTTPException(status_code=404, detail="Review not found")
review.status = ReviewStatus.approved
db.commit()
db.refresh(review)
return {
"status": "success",
"message": "Review approved successfully",
"data": {"review": review}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.put("/{id}/reject", dependencies=[Depends(authorize_roles("admin"))])
async def reject_review(
id: int,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Reject review (Admin only)"""
try:
review = db.query(Review).filter(Review.id == id).first()
if not review:
raise HTTPException(status_code=404, detail="Review not found")
review.status = ReviewStatus.rejected
db.commit()
db.refresh(review)
return {
"status": "success",
"message": "Review rejected successfully",
"data": {"review": review}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/{id}", dependencies=[Depends(authorize_roles("admin"))])
async def delete_review(
id: int,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Delete review (Admin only)"""
try:
review = db.query(Review).filter(Review.id == id).first()
if not review:
raise HTTPException(status_code=404, detail="Review not found")
db.delete(review)
db.commit()
return {
"status": "success",
"message": "Review deleted successfully"
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -0,0 +1,517 @@
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File, Request, Query
from sqlalchemy.orm import Session
from sqlalchemy import and_, or_, func
from typing import List, Optional
from datetime import datetime
from ..config.database import get_db
from ..middleware.auth import get_current_user, authorize_roles
from ..models.user import User
from ..models.room import Room, RoomStatus
from ..models.room_type import RoomType
from ..models.review import Review, ReviewStatus
from ..models.booking import Booking, BookingStatus
from ..services.room_service import get_rooms_with_ratings, get_amenities_list, normalize_images, get_base_url
import os
import aiofiles
from pathlib import Path
router = APIRouter(prefix="/rooms", tags=["rooms"])
@router.get("/")
async def get_rooms(
request: Request,
type: Optional[str] = Query(None),
minPrice: Optional[float] = Query(None),
maxPrice: Optional[float] = Query(None),
capacity: Optional[int] = Query(None),
page: int = Query(1, ge=1),
limit: int = Query(10, ge=1, le=100),
sort: Optional[str] = Query(None),
featured: Optional[bool] = Query(None),
db: Session = Depends(get_db)
):
"""Get all rooms with filters"""
try:
# Build where clause for rooms
where_clause = {}
room_type_where = {}
if featured is not None:
where_clause["featured"] = featured
if type:
room_type_where["name"] = f"%{type}%"
if capacity:
room_type_where["capacity"] = capacity
if minPrice or maxPrice:
if minPrice:
room_type_where["base_price_min"] = minPrice
if maxPrice:
room_type_where["base_price_max"] = maxPrice
# Build query
query = db.query(Room).join(RoomType)
# Apply filters
if where_clause.get("featured") is not None:
query = query.filter(Room.featured == where_clause["featured"])
if room_type_where.get("name"):
query = query.filter(RoomType.name.like(room_type_where["name"]))
if room_type_where.get("capacity"):
query = query.filter(RoomType.capacity >= room_type_where["capacity"])
if room_type_where.get("base_price_min"):
query = query.filter(RoomType.base_price >= room_type_where["base_price_min"])
if room_type_where.get("base_price_max"):
query = query.filter(RoomType.base_price <= room_type_where["base_price_max"])
# Get total count
total = query.count()
# Apply sorting
if sort == "newest" or sort == "created_at":
query = query.order_by(Room.created_at.desc())
else:
query = query.order_by(Room.featured.desc(), Room.created_at.desc())
# Apply pagination
offset = (page - 1) * limit
rooms = query.offset(offset).limit(limit).all()
# Get base URL
base_url = get_base_url(request)
# Get rooms with ratings
rooms_with_ratings = await get_rooms_with_ratings(db, rooms, base_url)
return {
"status": "success",
"data": {
"rooms": rooms_with_ratings,
"pagination": {
"total": total,
"page": page,
"limit": limit,
"totalPages": (total + limit - 1) // limit,
},
},
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/amenities")
async def get_amenities(db: Session = Depends(get_db)):
"""Get all available amenities"""
try:
amenities = await get_amenities_list(db)
return {"status": "success", "data": {"amenities": amenities}}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/available")
async def search_available_rooms(
request: Request,
from_date: str = Query(..., alias="from"),
to_date: str = Query(..., alias="to"),
type: Optional[str] = Query(None),
capacity: Optional[int] = Query(None),
page: int = Query(1, ge=1),
limit: int = Query(12, ge=1, le=100),
db: Session = Depends(get_db)
):
"""Search for available rooms"""
try:
check_in = datetime.fromisoformat(from_date.replace('Z', '+00:00'))
check_out = datetime.fromisoformat(to_date.replace('Z', '+00:00'))
if check_in >= check_out:
raise HTTPException(
status_code=400,
detail="Check-out date must be after check-in date"
)
# Build room type filter
query = db.query(Room).join(RoomType).filter(Room.status == RoomStatus.available)
if type:
query = query.filter(RoomType.name.like(f"%{type}%"))
if capacity:
query = query.filter(RoomType.capacity >= capacity)
# Exclude rooms with overlapping bookings
overlapping_rooms = db.query(Booking.room_id).filter(
and_(
Booking.status != BookingStatus.cancelled,
Booking.check_in_date < check_out,
Booking.check_out_date > check_in
)
).subquery()
query = query.filter(~Room.id.in_(db.query(overlapping_rooms.c.room_id)))
# Get total
total = query.count()
# Apply sorting and pagination
query = query.order_by(Room.featured.desc(), Room.created_at.desc())
offset = (page - 1) * limit
rooms = query.offset(offset).limit(limit).all()
# Get base URL
base_url = get_base_url(request)
# Get rooms with ratings
rooms_with_ratings = await get_rooms_with_ratings(db, rooms, base_url)
return {
"status": "success",
"data": {
"rooms": rooms_with_ratings,
"search": {
"from": from_date,
"to": to_date,
"type": type,
"capacity": capacity,
},
"pagination": {
"total": total,
"page": page,
"limit": limit,
"totalPages": (total + limit - 1) // limit,
},
},
}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/{id}")
async def get_room_by_id(id: int, request: Request, db: Session = Depends(get_db)):
"""Get room by ID"""
try:
room = db.query(Room).filter(Room.id == id).first()
if not room:
raise HTTPException(status_code=404, detail="Room not found")
# Get review stats
review_stats = db.query(
func.avg(Review.rating).label('average_rating'),
func.count(Review.id).label('total_reviews')
).filter(
and_(
Review.room_id == room.id,
Review.status == ReviewStatus.approved
)
).first()
base_url = get_base_url(request)
room_dict = {
"id": room.id,
"room_type_id": room.room_type_id,
"room_number": room.room_number,
"floor": room.floor,
"status": room.status.value if isinstance(room.status, RoomStatus) else room.status,
"price": float(room.price) if room.price else 0.0,
"featured": room.featured,
"description": room.description,
"amenities": room.amenities,
"created_at": room.created_at.isoformat() if room.created_at else None,
"updated_at": room.updated_at.isoformat() if room.updated_at else None,
"average_rating": round(float(review_stats.average_rating or 0), 1) if review_stats and review_stats.average_rating else None,
"total_reviews": review_stats.total_reviews or 0 if review_stats else 0,
}
# Normalize images
try:
room_dict["images"] = normalize_images(room.images, base_url)
except:
room_dict["images"] = []
# Add room type
if room.room_type:
room_dict["room_type"] = {
"id": room.room_type.id,
"name": room.room_type.name,
"description": room.room_type.description,
"base_price": float(room.room_type.base_price) if room.room_type.base_price else 0.0,
"capacity": room.room_type.capacity,
"amenities": room.room_type.amenities,
"images": [] # RoomType doesn't have images column in DB
}
return {
"status": "success",
"data": {"room": room_dict}
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/", dependencies=[Depends(authorize_roles("admin"))])
async def create_room(
room_data: dict,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Create new room (Admin only)"""
try:
# Check if room type exists
room_type = db.query(RoomType).filter(RoomType.id == room_data.get("room_type_id")).first()
if not room_type:
raise HTTPException(status_code=404, detail="Room type not found")
# Check if room number exists
existing = db.query(Room).filter(Room.room_number == room_data.get("room_number")).first()
if existing:
raise HTTPException(status_code=400, detail="Room number already exists")
room = Room(
room_type_id=room_data.get("room_type_id"),
room_number=room_data.get("room_number"),
floor=room_data.get("floor"),
status=RoomStatus(room_data.get("status", "available")),
featured=room_data.get("featured", False),
price=room_data.get("price", room_type.base_price),
)
db.add(room)
db.commit()
db.refresh(room)
return {
"status": "success",
"message": "Room created successfully",
"data": {"room": room}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.put("/{id}", dependencies=[Depends(authorize_roles("admin"))])
async def update_room(
id: int,
room_data: dict,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Update room (Admin only)"""
try:
room = db.query(Room).filter(Room.id == id).first()
if not room:
raise HTTPException(status_code=404, detail="Room not found")
if room_data.get("room_type_id"):
room_type = db.query(RoomType).filter(RoomType.id == room_data["room_type_id"]).first()
if not room_type:
raise HTTPException(status_code=404, detail="Room type not found")
# Update fields
if "room_type_id" in room_data:
room.room_type_id = room_data["room_type_id"]
if "room_number" in room_data:
room.room_number = room_data["room_number"]
if "floor" in room_data:
room.floor = room_data["floor"]
if "status" in room_data:
room.status = RoomStatus(room_data["status"])
if "featured" in room_data:
room.featured = room_data["featured"]
if "price" in room_data:
room.price = room_data["price"]
db.commit()
db.refresh(room)
return {
"status": "success",
"message": "Room updated successfully",
"data": {"room": room}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/{id}", dependencies=[Depends(authorize_roles("admin"))])
async def delete_room(
id: int,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Delete room (Admin only)"""
try:
room = db.query(Room).filter(Room.id == id).first()
if not room:
raise HTTPException(status_code=404, detail="Room not found")
db.delete(room)
db.commit()
return {
"status": "success",
"message": "Room deleted successfully"
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.post("/{id}/images", dependencies=[Depends(authorize_roles("admin", "staff"))])
async def upload_room_images(
id: int,
images: List[UploadFile] = File(...),
current_user: User = Depends(authorize_roles("admin", "staff")),
db: Session = Depends(get_db)
):
"""Upload room images (Admin/Staff only)"""
try:
room = db.query(Room).filter(Room.id == id).first()
if not room:
raise HTTPException(status_code=404, detail="Room not found")
# Create uploads directory
upload_dir = Path(__file__).parent.parent.parent / "uploads" / "rooms"
upload_dir.mkdir(parents=True, exist_ok=True)
image_urls = []
for image in images:
# Validate file type
if not image.content_type.startswith('image/'):
continue
# Generate filename
import uuid
ext = Path(image.filename).suffix
filename = f"room-{uuid.uuid4()}{ext}"
file_path = upload_dir / filename
# Save file
async with aiofiles.open(file_path, 'wb') as f:
content = await image.read()
await f.write(content)
image_urls.append(f"/uploads/rooms/{filename}")
# Update room images (images are stored on Room, not RoomType)
existing_images = room.images or []
updated_images = existing_images + image_urls
room.images = updated_images
db.commit()
return {
"status": "success",
"message": "Images uploaded successfully",
"data": {"images": updated_images}
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/{id}/images", dependencies=[Depends(authorize_roles("admin", "staff"))])
async def delete_room_images(
id: int,
image_url: str,
current_user: User = Depends(authorize_roles("admin", "staff")),
db: Session = Depends(get_db)
):
"""Delete room images (Admin/Staff only)"""
try:
room = db.query(Room).filter(Room.id == id).first()
if not room:
raise HTTPException(status_code=404, detail="Room not found")
# Update room images (images are stored on Room, not RoomType)
existing_images = room.images or []
updated_images = [img for img in existing_images if img != image_url]
# Delete file from disk
filename = Path(image_url).name
file_path = Path(__file__).parent.parent.parent / "uploads" / "rooms" / filename
if file_path.exists():
file_path.unlink()
room.images = updated_images
db.commit()
return {
"status": "success",
"message": "Image deleted successfully",
"data": {"images": updated_images}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.get("/{id}/reviews")
async def get_room_reviews_route(
id: int,
db: Session = Depends(get_db)
):
"""Get reviews for a specific room"""
from ..models.review import Review, ReviewStatus
try:
room = db.query(Room).filter(Room.id == id).first()
if not room:
raise HTTPException(status_code=404, detail="Room not found")
reviews = db.query(Review).filter(
Review.room_id == id,
Review.status == ReviewStatus.approved
).order_by(Review.created_at.desc()).all()
result = []
for review in reviews:
review_dict = {
"id": review.id,
"user_id": review.user_id,
"room_id": review.room_id,
"rating": review.rating,
"comment": review.comment,
"status": review.status.value if isinstance(review.status, ReviewStatus) else review.status,
"created_at": review.created_at.isoformat() if review.created_at else None,
}
if review.user:
review_dict["user"] = {
"id": review.user.id,
"full_name": review.user.full_name,
"email": review.user.email,
}
result.append(review_dict)
return {
"status": "success",
"data": {"reviews": result}
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -0,0 +1,277 @@
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from sqlalchemy import or_
from typing import Optional
from ..config.database import get_db
from ..middleware.auth import get_current_user, authorize_roles
from ..models.user import User
from ..models.service import Service
from ..models.service_usage import ServiceUsage
from ..models.booking import Booking, BookingStatus
router = APIRouter(prefix="/services", tags=["services"])
@router.get("/")
async def get_services(
search: Optional[str] = Query(None),
status_filter: Optional[str] = Query(None, alias="status"),
page: int = Query(1, ge=1),
limit: int = Query(10, ge=1, le=100),
db: Session = Depends(get_db)
):
"""Get all services with filters"""
try:
query = db.query(Service)
# Filter by search (name or description)
if search:
query = query.filter(
or_(
Service.name.like(f"%{search}%"),
Service.description.like(f"%{search}%")
)
)
# Filter by status (is_active)
if status_filter:
is_active = status_filter == "active"
query = query.filter(Service.is_active == is_active)
total = query.count()
offset = (page - 1) * limit
services = query.order_by(Service.created_at.desc()).offset(offset).limit(limit).all()
result = []
for service in services:
service_dict = {
"id": service.id,
"name": service.name,
"description": service.description,
"price": float(service.price) if service.price else 0.0,
"category": service.category,
"is_active": service.is_active,
"created_at": service.created_at.isoformat() if service.created_at else None,
}
result.append(service_dict)
return {
"status": "success",
"data": {
"services": result,
"pagination": {
"total": total,
"page": page,
"limit": limit,
"totalPages": (total + limit - 1) // limit,
},
},
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/{id}")
async def get_service_by_id(id: int, db: Session = Depends(get_db)):
"""Get service by ID"""
try:
service = db.query(Service).filter(Service.id == id).first()
if not service:
raise HTTPException(status_code=404, detail="Service not found")
service_dict = {
"id": service.id,
"name": service.name,
"description": service.description,
"price": float(service.price) if service.price else 0.0,
"category": service.category,
"is_active": service.is_active,
"created_at": service.created_at.isoformat() if service.created_at else None,
}
return {
"status": "success",
"data": {"service": service_dict}
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/", dependencies=[Depends(authorize_roles("admin"))])
async def create_service(
service_data: dict,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Create new service (Admin only)"""
try:
name = service_data.get("name")
# Check if name exists
existing = db.query(Service).filter(Service.name == name).first()
if existing:
raise HTTPException(status_code=400, detail="Service name already exists")
service = Service(
name=name,
description=service_data.get("description"),
price=float(service_data.get("price", 0)),
category=service_data.get("category"),
is_active=service_data.get("status") == "active" if service_data.get("status") else True,
)
db.add(service)
db.commit()
db.refresh(service)
return {
"status": "success",
"message": "Service created successfully",
"data": {"service": service}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.put("/{id}", dependencies=[Depends(authorize_roles("admin"))])
async def update_service(
id: int,
service_data: dict,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Update service (Admin only)"""
try:
service = db.query(Service).filter(Service.id == id).first()
if not service:
raise HTTPException(status_code=404, detail="Service not found")
# Check if new name exists (excluding current)
name = service_data.get("name")
if name and name != service.name:
existing = db.query(Service).filter(
Service.name == name,
Service.id != id
).first()
if existing:
raise HTTPException(status_code=400, detail="Service name already exists")
# Update fields
if "name" in service_data:
service.name = service_data["name"]
if "description" in service_data:
service.description = service_data["description"]
if "price" in service_data:
service.price = float(service_data["price"])
if "category" in service_data:
service.category = service_data["category"]
if "status" in service_data:
service.is_active = service_data["status"] == "active"
db.commit()
db.refresh(service)
return {
"status": "success",
"message": "Service updated successfully",
"data": {"service": service}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/{id}", dependencies=[Depends(authorize_roles("admin"))])
async def delete_service(
id: int,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Delete service (Admin only)"""
try:
service = db.query(Service).filter(Service.id == id).first()
if not service:
raise HTTPException(status_code=404, detail="Service not found")
# Check if service is used in active bookings
active_usage = db.query(ServiceUsage).join(Booking).filter(
ServiceUsage.service_id == id,
Booking.status.in_([BookingStatus.pending, BookingStatus.confirmed, BookingStatus.checked_in])
).count()
if active_usage > 0:
raise HTTPException(
status_code=400,
detail="Cannot delete service that is used in active bookings"
)
db.delete(service)
db.commit()
return {
"status": "success",
"message": "Service deleted successfully"
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.post("/use")
async def use_service(
usage_data: dict,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Add service to booking"""
try:
booking_id = usage_data.get("booking_id")
service_id = usage_data.get("service_id")
quantity = usage_data.get("quantity", 1)
# Check if booking exists
booking = db.query(Booking).filter(Booking.id == booking_id).first()
if not booking:
raise HTTPException(status_code=404, detail="Booking not found")
# Check if service exists and is active
service = db.query(Service).filter(Service.id == service_id).first()
if not service or not service.is_active:
raise HTTPException(status_code=404, detail="Service not found or inactive")
# Calculate total price
total_price = float(service.price) * quantity
# Create service usage
service_usage = ServiceUsage(
booking_id=booking_id,
service_id=service_id,
quantity=quantity,
unit_price=service.price,
total_price=total_price,
)
db.add(service_usage)
db.commit()
db.refresh(service_usage)
return {
"status": "success",
"message": "Service added to booking successfully",
"data": {"bookingService": service_usage}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -0,0 +1,317 @@
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from sqlalchemy import or_
from typing import Optional
import bcrypt
from ..config.database import get_db
from ..middleware.auth import get_current_user, authorize_roles
from ..models.user import User
from ..models.role import Role
from ..models.booking import Booking, BookingStatus
router = APIRouter(prefix="/users", tags=["users"])
@router.get("/", dependencies=[Depends(authorize_roles("admin"))])
async def get_users(
search: Optional[str] = Query(None),
role: Optional[str] = Query(None),
status_filter: Optional[str] = Query(None, alias="status"),
page: int = Query(1, ge=1),
limit: int = Query(10, ge=1, le=100),
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Get all users with filters and pagination (Admin only)"""
try:
query = db.query(User)
# Filter by search (full_name, email, phone)
if search:
query = query.filter(
or_(
User.full_name.like(f"%{search}%"),
User.email.like(f"%{search}%"),
User.phone.like(f"%{search}%")
)
)
# Filter by role
if role:
role_map = {"admin": 1, "staff": 2, "customer": 3}
if role in role_map:
query = query.filter(User.role_id == role_map[role])
# Filter by status
if status_filter:
is_active = status_filter == "active"
query = query.filter(User.is_active == is_active)
# Get total count
total = query.count()
# Apply pagination
offset = (page - 1) * limit
users = query.order_by(User.created_at.desc()).offset(offset).limit(limit).all()
# Transform users
result = []
for user in users:
user_dict = {
"id": user.id,
"email": user.email,
"full_name": user.full_name,
"phone": user.phone,
"phone_number": user.phone, # For frontend compatibility
"address": user.address,
"avatar": user.avatar,
"is_active": user.is_active,
"status": "active" if user.is_active else "inactive",
"role_id": user.role_id,
"role": user.role.name if user.role else "customer",
"created_at": user.created_at.isoformat() if user.created_at else None,
"updated_at": user.updated_at.isoformat() if user.updated_at else None,
}
result.append(user_dict)
return {
"status": "success",
"data": {
"users": result,
"pagination": {
"total": total,
"page": page,
"limit": limit,
"totalPages": (total + limit - 1) // limit,
},
},
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/{id}", dependencies=[Depends(authorize_roles("admin"))])
async def get_user_by_id(
id: int,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Get user by ID (Admin only)"""
try:
user = db.query(User).filter(User.id == id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
# Get recent bookings
bookings = db.query(Booking).filter(
Booking.user_id == id
).order_by(Booking.created_at.desc()).limit(5).all()
user_dict = {
"id": user.id,
"email": user.email,
"full_name": user.full_name,
"phone": user.phone,
"phone_number": user.phone,
"address": user.address,
"avatar": user.avatar,
"is_active": user.is_active,
"status": "active" if user.is_active else "inactive",
"role_id": user.role_id,
"role": user.role.name if user.role else "customer",
"created_at": user.created_at.isoformat() if user.created_at else None,
"updated_at": user.updated_at.isoformat() if user.updated_at else None,
"bookings": [
{
"id": b.id,
"booking_number": b.booking_number,
"status": b.status.value if isinstance(b.status, BookingStatus) else b.status,
"created_at": b.created_at.isoformat() if b.created_at else None,
}
for b in bookings
],
}
return {
"status": "success",
"data": {"user": user_dict}
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/", dependencies=[Depends(authorize_roles("admin"))])
async def create_user(
user_data: dict,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Create new user (Admin only)"""
try:
email = user_data.get("email")
password = user_data.get("password")
full_name = user_data.get("full_name")
phone_number = user_data.get("phone_number")
role = user_data.get("role", "customer")
status = user_data.get("status", "active")
# Map role string to role_id
role_map = {"admin": 1, "staff": 2, "customer": 3}
role_id = role_map.get(role, 3)
# Check if email exists
existing = db.query(User).filter(User.email == email).first()
if existing:
raise HTTPException(status_code=400, detail="Email already exists")
# Hash password
password_bytes = password.encode('utf-8')
salt = bcrypt.gensalt()
hashed_password = bcrypt.hashpw(password_bytes, salt).decode('utf-8')
# Create user
user = User(
email=email,
password=hashed_password,
full_name=full_name,
phone=phone_number,
role_id=role_id,
is_active=status == "active",
)
db.add(user)
db.commit()
db.refresh(user)
# Remove password from response
user_dict = {
"id": user.id,
"email": user.email,
"full_name": user.full_name,
"phone": user.phone,
"phone_number": user.phone,
"role_id": user.role_id,
"is_active": user.is_active,
}
return {
"status": "success",
"message": "User created successfully",
"data": {"user": user_dict}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.put("/{id}")
async def update_user(
id: int,
user_data: dict,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""Update user"""
try:
# Users can only update themselves unless they're admin
if current_user.role_id != 1 and current_user.id != id:
raise HTTPException(status_code=403, detail="Forbidden")
user = db.query(User).filter(User.id == id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
# Check if email is being changed and if it's taken
email = user_data.get("email")
if email and email != user.email:
existing = db.query(User).filter(User.email == email).first()
if existing:
raise HTTPException(status_code=400, detail="Email already exists")
# Map role string to role_id (only admin can change role)
role_map = {"admin": 1, "staff": 2, "customer": 3}
# Update fields
if "full_name" in user_data:
user.full_name = user_data["full_name"]
if "email" in user_data and current_user.role_id == 1:
user.email = user_data["email"]
if "phone_number" in user_data:
user.phone = user_data["phone_number"]
if "role" in user_data and current_user.role_id == 1:
user.role_id = role_map.get(user_data["role"], 3)
if "status" in user_data and current_user.role_id == 1:
user.is_active = user_data["status"] == "active"
if "password" in user_data:
password_bytes = user_data["password"].encode('utf-8')
salt = bcrypt.gensalt()
user.password = bcrypt.hashpw(password_bytes, salt).decode('utf-8')
db.commit()
db.refresh(user)
# Remove password from response
user_dict = {
"id": user.id,
"email": user.email,
"full_name": user.full_name,
"phone": user.phone,
"phone_number": user.phone,
"role_id": user.role_id,
"is_active": user.is_active,
}
return {
"status": "success",
"message": "User updated successfully",
"data": {"user": user_dict}
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/{id}", dependencies=[Depends(authorize_roles("admin"))])
async def delete_user(
id: int,
current_user: User = Depends(authorize_roles("admin")),
db: Session = Depends(get_db)
):
"""Delete user (Admin only)"""
try:
user = db.query(User).filter(User.id == id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
# Check if user has active bookings
active_bookings = db.query(Booking).filter(
Booking.user_id == id,
Booking.status.in_([BookingStatus.pending, BookingStatus.confirmed, BookingStatus.checked_in])
).count()
if active_bookings > 0:
raise HTTPException(
status_code=400,
detail="Cannot delete user with active bookings"
)
db.delete(user)
db.commit()
return {
"status": "success",
"message": "User deleted successfully"
}
except HTTPException:
raise
except Exception as e:
db.rollback()
raise HTTPException(status_code=500, detail=str(e))

Binary file not shown.

View File

@@ -0,0 +1,87 @@
from pydantic import BaseModel, EmailStr, Field, validator
from typing import Optional
class RegisterRequest(BaseModel):
name: str = Field(..., min_length=2, max_length=50)
email: EmailStr
password: str = Field(..., min_length=8)
phone: Optional[str] = None
@validator("password")
def validate_password(cls, v):
if len(v) < 8:
raise ValueError("Password must be at least 8 characters")
if not any(c.isupper() for c in v):
raise ValueError("Password must contain at least one uppercase letter")
if not any(c.islower() for c in v):
raise ValueError("Password must contain at least one lowercase letter")
if not any(c.isdigit() for c in v):
raise ValueError("Password must contain at least one number")
return v
@validator("phone")
def validate_phone(cls, v):
if v and not v.isdigit() or (v and len(v) not in [10, 11]):
raise ValueError("Phone must be 10-11 digits")
return v
class LoginRequest(BaseModel):
email: EmailStr
password: str
rememberMe: Optional[bool] = False
class RefreshTokenRequest(BaseModel):
refreshToken: Optional[str] = None
class ForgotPasswordRequest(BaseModel):
email: EmailStr
class ResetPasswordRequest(BaseModel):
token: str
password: str = Field(..., min_length=8)
@validator("password")
def validate_password(cls, v):
if len(v) < 8:
raise ValueError("Password must be at least 8 characters")
if not any(c.isupper() for c in v):
raise ValueError("Password must contain at least one uppercase letter")
if not any(c.islower() for c in v):
raise ValueError("Password must contain at least one lowercase letter")
if not any(c.isdigit() for c in v):
raise ValueError("Password must contain at least one number")
return v
class UserResponse(BaseModel):
id: int
name: str
email: str
phone: Optional[str]
role: str
createdAt: Optional[str]
updatedAt: Optional[str]
class Config:
from_attributes = True
class AuthResponse(BaseModel):
user: UserResponse
token: str
refreshToken: Optional[str] = None
class TokenResponse(BaseModel):
token: str
class MessageResponse(BaseModel):
status: str
message: str

View File

@@ -0,0 +1,350 @@
from jose import jwt
import bcrypt
from datetime import datetime, timedelta
import secrets
import hashlib
from sqlalchemy.orm import Session
from typing import Optional
from ..models.user import User
from ..models.refresh_token import RefreshToken
from ..models.password_reset_token import PasswordResetToken
from ..models.role import Role
from ..utils.mailer import send_email
import os
class AuthService:
def __init__(self):
self.jwt_secret = os.getenv("JWT_SECRET")
self.jwt_refresh_secret = os.getenv("JWT_REFRESH_SECRET")
self.jwt_expires_in = os.getenv("JWT_EXPIRES_IN", "1h")
self.jwt_refresh_expires_in = os.getenv("JWT_REFRESH_EXPIRES_IN", "7d")
def generate_tokens(self, user_id: int) -> dict:
"""Generate JWT tokens"""
access_token = jwt.encode(
{"userId": user_id},
self.jwt_secret,
algorithm="HS256"
)
refresh_token = jwt.encode(
{"userId": user_id},
self.jwt_refresh_secret,
algorithm="HS256"
)
return {"accessToken": access_token, "refreshToken": refresh_token}
def verify_access_token(self, token: str) -> dict:
"""Verify JWT access token"""
return jwt.decode(token, self.jwt_secret, algorithms=["HS256"])
def verify_refresh_token(self, token: str) -> dict:
"""Verify JWT refresh token"""
return jwt.decode(token, self.jwt_refresh_secret, algorithms=["HS256"])
def hash_password(self, password: str) -> str:
"""Hash password using bcrypt"""
# bcrypt has 72 byte limit, but it handles truncation automatically
password_bytes = password.encode('utf-8')
# Generate salt and hash password
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(password_bytes, salt)
return hashed.decode('utf-8')
def verify_password(self, plain_password: str, hashed_password: str) -> bool:
"""Verify password using bcrypt"""
try:
password_bytes = plain_password.encode('utf-8')
hashed_bytes = hashed_password.encode('utf-8')
return bcrypt.checkpw(password_bytes, hashed_bytes)
except Exception:
return False
def format_user_response(self, user: User) -> dict:
"""Format user response"""
return {
"id": user.id,
"name": user.full_name,
"email": user.email,
"phone": user.phone,
"role": user.role.name if user.role else "customer",
"createdAt": user.created_at.isoformat() if user.created_at else None,
"updatedAt": user.updated_at.isoformat() if user.updated_at else None,
}
async def register(self, db: Session, name: str, email: str, password: str, phone: Optional[str] = None) -> dict:
"""Register new user"""
# Check if email exists
existing_user = db.query(User).filter(User.email == email).first()
if existing_user:
raise ValueError("Email already registered")
# Hash password
hashed_password = self.hash_password(password)
# Create user (default role_id = 3 for customer)
user = User(
full_name=name,
email=email,
password=hashed_password,
phone=phone,
role_id=3 # Customer role
)
db.add(user)
db.commit()
db.refresh(user)
# Load role
user.role = db.query(Role).filter(Role.id == user.role_id).first()
# Generate tokens
tokens = self.generate_tokens(user.id)
# Save refresh token (expires in 7 days)
expires_at = datetime.utcnow() + timedelta(days=7)
refresh_token = RefreshToken(
user_id=user.id,
token=tokens["refreshToken"],
expires_at=expires_at
)
db.add(refresh_token)
db.commit()
# Send welcome email (non-blocking)
try:
client_url = os.getenv("CLIENT_URL", "http://localhost:5173")
await send_email(
to=user.email,
subject="Welcome to Hotel Booking",
html=f"""
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h2 style="color: #4F46E5;">Welcome {user.full_name}!</h2>
<p>Thank you for registering an account at <strong>Hotel Booking</strong>.</p>
<p>Your account has been successfully created with email: <strong>{user.email}</strong></p>
<div style="background-color: #F3F4F6; padding: 20px; border-radius: 8px; margin: 20px 0;">
<p style="margin: 0;"><strong>You can:</strong></p>
<ul style="margin-top: 10px;">
<li>Search and book hotel rooms</li>
<li>Manage your bookings</li>
<li>Update your personal information</li>
</ul>
</div>
<p>
<a href="{client_url}/login" style="background-color: #4F46E5; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;">
Login Now
</a>
</p>
</div>
"""
)
except Exception as e:
print(f"Failed to send welcome email: {e}")
return {
"user": self.format_user_response(user),
"token": tokens["accessToken"],
"refreshToken": tokens["refreshToken"]
}
async def login(self, db: Session, email: str, password: str, remember_me: bool = False) -> dict:
"""Login user"""
# Find user with role and password
user = db.query(User).filter(User.email == email).first()
if not user:
raise ValueError("Invalid email or password")
# Load role
user.role = db.query(Role).filter(Role.id == user.role_id).first()
# Check password
if not self.verify_password(password, user.password):
raise ValueError("Invalid email or password")
# Generate tokens
tokens = self.generate_tokens(user.id)
# Calculate expiry based on remember_me
expiry_days = 7 if remember_me else 1
expires_at = datetime.utcnow() + timedelta(days=expiry_days)
# Save refresh token
refresh_token = RefreshToken(
user_id=user.id,
token=tokens["refreshToken"],
expires_at=expires_at
)
db.add(refresh_token)
db.commit()
return {
"user": self.format_user_response(user),
"token": tokens["accessToken"],
"refreshToken": tokens["refreshToken"]
}
async def refresh_access_token(self, db: Session, refresh_token_str: str) -> dict:
"""Refresh access token"""
if not refresh_token_str:
raise ValueError("Refresh token is required")
# Verify refresh token
decoded = self.verify_refresh_token(refresh_token_str)
# Check if refresh token exists in database
stored_token = db.query(RefreshToken).filter(
RefreshToken.token == refresh_token_str,
RefreshToken.user_id == decoded["userId"]
).first()
if not stored_token:
raise ValueError("Invalid refresh token")
# Check if token is expired
if datetime.utcnow() > stored_token.expires_at:
db.delete(stored_token)
db.commit()
raise ValueError("Refresh token expired")
# Generate new access token
access_token = jwt.encode(
{"userId": decoded["userId"]},
self.jwt_secret,
algorithm="HS256"
)
return {"token": access_token}
async def logout(self, db: Session, refresh_token_str: str) -> bool:
"""Logout user"""
if refresh_token_str:
db.query(RefreshToken).filter(RefreshToken.token == refresh_token_str).delete()
db.commit()
return True
async def get_profile(self, db: Session, user_id: int) -> dict:
"""Get user profile"""
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise ValueError("User not found")
# Load role
user.role = db.query(Role).filter(Role.id == user.role_id).first()
return self.format_user_response(user)
def generate_reset_token(self) -> tuple:
"""Generate reset token"""
reset_token = secrets.token_hex(32)
hashed_token = hashlib.sha256(reset_token.encode()).hexdigest()
return reset_token, hashed_token
async def forgot_password(self, db: Session, email: str) -> dict:
"""Forgot Password - Send reset link"""
# Find user by email
user = db.query(User).filter(User.email == email).first()
# Always return success to prevent email enumeration
if not user:
return {
"success": True,
"message": "If email exists, reset link has been sent"
}
# Generate reset token
reset_token, hashed_token = self.generate_reset_token()
# Delete old tokens
db.query(PasswordResetToken).filter(PasswordResetToken.user_id == user.id).delete()
# Save token (expires in 1 hour)
expires_at = datetime.utcnow() + timedelta(hours=1)
reset_token_obj = PasswordResetToken(
user_id=user.id,
token=hashed_token,
expires_at=expires_at
)
db.add(reset_token_obj)
db.commit()
# Build reset URL
client_url = os.getenv("CLIENT_URL", "http://localhost:5173")
reset_url = f"{client_url}/reset-password/{reset_token}"
# Try to send email
try:
await send_email(
to=user.email,
subject="Reset password - Hotel Booking",
html=f"""
<p>You (or someone) has requested to reset your password.</p>
<p>Click the link below to reset your password (expires in 1 hour):</p>
<p><a href="{reset_url}">{reset_url}</a></p>
"""
)
except Exception as e:
print(f"Failed to send reset email: {e}")
return {
"success": True,
"message": "Password reset link has been sent to your email"
}
async def reset_password(self, db: Session, token: str, password: str) -> dict:
"""Reset Password - Update password with token"""
if not token or not password:
raise ValueError("Token and password are required")
# Hash the token to compare
hashed_token = hashlib.sha256(token.encode()).hexdigest()
# Find valid token
reset_token = db.query(PasswordResetToken).filter(
PasswordResetToken.token == hashed_token,
PasswordResetToken.expires_at > datetime.utcnow(),
PasswordResetToken.used == False
).first()
if not reset_token:
raise ValueError("Invalid or expired reset token")
# Find user
user = db.query(User).filter(User.id == reset_token.user_id).first()
if not user:
raise ValueError("User not found")
# Check if new password matches old password
if self.verify_password(password, user.password):
raise ValueError("New password must be different from the old password")
# Hash new password
hashed_password = self.hash_password(password)
# Update password
user.password = hashed_password
db.commit()
# Mark token as used
reset_token.used = True
db.commit()
# Send confirmation email (non-blocking)
try:
await send_email(
to=user.email,
subject="Password Changed",
html=f"<p>The password for account {user.email} has been changed successfully.</p>"
)
except Exception as e:
print(f"Failed to send confirmation email: {e}")
return {
"success": True,
"message": "Password has been reset successfully"
}
auth_service = AuthService()

View File

@@ -0,0 +1,145 @@
from sqlalchemy.orm import Session
from sqlalchemy import func, and_, or_
from typing import Optional, List, Dict
from datetime import datetime
import os
from ..models.room import Room, RoomStatus
from ..models.room_type import RoomType
from ..models.review import Review, ReviewStatus
def normalize_images(images, base_url: str) -> List[str]:
"""Normalize image paths to absolute URLs"""
if not images:
return []
imgs = images
if isinstance(images, str):
try:
import json
imgs = json.loads(images)
except:
imgs = [s.strip() for s in images.split(',') if s.strip()]
if not isinstance(imgs, list):
return []
result = []
for img in imgs:
if not img:
continue
if img.startswith('http://') or img.startswith('https://'):
result.append(img)
else:
path_part = img if img.startswith('/') else f"/{img}"
result.append(f"{base_url}{path_part}")
return result
def get_base_url(request) -> str:
"""Get base URL for image normalization"""
return os.getenv("SERVER_URL") or f"http://{request.headers.get('host', 'localhost:3000')}"
async def get_rooms_with_ratings(
db: Session,
rooms: List[Room],
base_url: str
) -> List[Dict]:
"""Get rooms with calculated ratings"""
result = []
for room in rooms:
# Get review stats
review_stats = db.query(
func.avg(Review.rating).label('average_rating'),
func.count(Review.id).label('total_reviews')
).filter(
and_(
Review.room_id == room.id,
Review.status == ReviewStatus.approved
)
).first()
room_dict = {
"id": room.id,
"room_type_id": room.room_type_id,
"room_number": room.room_number,
"floor": room.floor,
"status": room.status.value if isinstance(room.status, RoomStatus) else room.status,
"price": float(room.price) if room.price else 0.0,
"featured": room.featured,
"description": room.description,
"amenities": room.amenities,
"created_at": room.created_at.isoformat() if room.created_at else None,
"updated_at": room.updated_at.isoformat() if room.updated_at else None,
"average_rating": round(float(review_stats.average_rating or 0), 1) if review_stats and review_stats.average_rating else None,
"total_reviews": review_stats.total_reviews or 0 if review_stats else 0,
}
# Normalize images
try:
room_dict["images"] = normalize_images(room.images, base_url)
except:
room_dict["images"] = []
# Add room type info
if room.room_type:
room_dict["room_type"] = {
"id": room.room_type.id,
"name": room.room_type.name,
"description": room.room_type.description,
"base_price": float(room.room_type.base_price) if room.room_type.base_price else 0.0,
"capacity": room.room_type.capacity,
"amenities": room.room_type.amenities,
"images": [] # RoomType doesn't have images column in DB
}
result.append(room_dict)
return result
async def get_amenities_list(db: Session) -> List[str]:
"""Get all unique amenities from room types and rooms"""
all_amenities = []
# Get from room types
room_types = db.query(RoomType.amenities).all()
for rt in room_types:
if rt.amenities:
if isinstance(rt.amenities, list):
all_amenities.extend([str(a).strip() for a in rt.amenities])
elif isinstance(rt.amenities, str):
try:
import json
parsed = json.loads(rt.amenities)
if isinstance(parsed, list):
all_amenities.extend([str(a).strip() for a in parsed])
else:
all_amenities.extend([s.strip() for s in rt.amenities.split(',')])
except:
all_amenities.extend([s.strip() for s in rt.amenities.split(',')])
# Get from rooms
rooms = db.query(Room.amenities).all()
for r in rooms:
if r.amenities:
if isinstance(r.amenities, list):
all_amenities.extend([str(a).strip() for a in r.amenities])
elif isinstance(r.amenities, str):
try:
import json
parsed = json.loads(r.amenities)
if isinstance(parsed, list):
all_amenities.extend([str(a).strip() for a in parsed])
else:
all_amenities.extend([s.strip() for s in r.amenities.split(',')])
except:
all_amenities.extend([s.strip() for s in r.amenities.split(',')])
# Return unique, non-empty values
return sorted(list(set([a for a in all_amenities if a])))

Binary file not shown.

View File

@@ -0,0 +1,48 @@
import aiosmtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import os
async def send_email(to: str, subject: str, html: str = None, text: str = None):
"""
Send email using SMTP
Requires MAIL_HOST, MAIL_USER and MAIL_PASS to be set in env.
"""
# Require SMTP credentials to be present
mail_host = os.getenv("MAIL_HOST")
mail_user = os.getenv("MAIL_USER")
mail_pass = os.getenv("MAIL_PASS")
if not (mail_host and mail_user and mail_pass):
raise ValueError(
"SMTP mailer not configured. Set MAIL_HOST, MAIL_USER and MAIL_PASS in env."
)
mail_port = int(os.getenv("MAIL_PORT", "587"))
mail_secure = os.getenv("MAIL_SECURE", "false").lower() == "true"
client_url = os.getenv("CLIENT_URL", "example.com")
from_address = os.getenv("MAIL_FROM", f"no-reply@{client_url.replace('https://', '').replace('http://', '')}")
# Create message
message = MIMEMultipart("alternative")
message["From"] = from_address
message["To"] = to
message["Subject"] = subject
if text:
message.attach(MIMEText(text, "plain"))
if html:
message.attach(MIMEText(html, "html"))
# Send email
await aiosmtplib.send(
message,
hostname=mail_host,
port=mail_port,
use_tls=not mail_secure and mail_port == 587,
start_tls=not mail_secure and mail_port == 587,
username=mail_user,
password=mail_pass,
)

View File

@@ -0,0 +1,22 @@
"""
VNPay integration removed
This file is intentionally left as a stub to indicate the VNPay
payment gateway has been removed from the project.
"""
def create_payment_url(*args, **kwargs):
raise NotImplementedError("VNPay integration has been removed")
def verify_return(*args, **kwargs):
raise NotImplementedError("VNPay integration has been removed")
def sort_object(obj):
return {}
def create_signature(*args, **kwargs):
return ""

View File

@@ -0,0 +1,247 @@
<#
.Synopsis
Activate a Python virtual environment for the current PowerShell session.
.Description
Pushes the python executable for a virtual environment to the front of the
$Env:PATH environment variable and sets the prompt to signify that you are
in a Python virtual environment. Makes use of the command line switches as
well as the `pyvenv.cfg` file values present in the virtual environment.
.Parameter VenvDir
Path to the directory that contains the virtual environment to activate. The
default value for this is the parent of the directory that the Activate.ps1
script is located within.
.Parameter Prompt
The prompt prefix to display when this virtual environment is activated. By
default, this prompt is the name of the virtual environment folder (VenvDir)
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
.Example
Activate.ps1
Activates the Python virtual environment that contains the Activate.ps1 script.
.Example
Activate.ps1 -Verbose
Activates the Python virtual environment that contains the Activate.ps1 script,
and shows extra information about the activation as it executes.
.Example
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
Activates the Python virtual environment located in the specified location.
.Example
Activate.ps1 -Prompt "MyPython"
Activates the Python virtual environment that contains the Activate.ps1 script,
and prefixes the current prompt with the specified string (surrounded in
parentheses) while the virtual environment is active.
.Notes
On Windows, it may be required to enable this Activate.ps1 script by setting the
execution policy for the user. You can do this by issuing the following PowerShell
command:
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
For more information on Execution Policies:
https://go.microsoft.com/fwlink/?LinkID=135170
#>
Param(
[Parameter(Mandatory = $false)]
[String]
$VenvDir,
[Parameter(Mandatory = $false)]
[String]
$Prompt
)
<# Function declarations --------------------------------------------------- #>
<#
.Synopsis
Remove all shell session elements added by the Activate script, including the
addition of the virtual environment's Python executable from the beginning of
the PATH variable.
.Parameter NonDestructive
If present, do not remove this function from the global namespace for the
session.
#>
function global:deactivate ([switch]$NonDestructive) {
# Revert to original values
# The prior prompt:
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
}
# The prior PYTHONHOME:
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
}
# The prior PATH:
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
}
# Just remove the VIRTUAL_ENV altogether:
if (Test-Path -Path Env:VIRTUAL_ENV) {
Remove-Item -Path env:VIRTUAL_ENV
}
# Just remove VIRTUAL_ENV_PROMPT altogether.
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
}
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
}
# Leave deactivate function in the global namespace if requested:
if (-not $NonDestructive) {
Remove-Item -Path function:deactivate
}
}
<#
.Description
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
given folder, and returns them in a map.
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
two strings separated by `=` (with any amount of whitespace surrounding the =)
then it is considered a `key = value` line. The left hand string is the key,
the right hand is the value.
If the value starts with a `'` or a `"` then the first and last character is
stripped from the value before being captured.
.Parameter ConfigDir
Path to the directory that contains the `pyvenv.cfg` file.
#>
function Get-PyVenvConfig(
[String]
$ConfigDir
) {
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
# An empty map will be returned if no config file is found.
$pyvenvConfig = @{ }
if ($pyvenvConfigPath) {
Write-Verbose "File exists, parse `key = value` lines"
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
$pyvenvConfigContent | ForEach-Object {
$keyval = $PSItem -split "\s*=\s*", 2
if ($keyval[0] -and $keyval[1]) {
$val = $keyval[1]
# Remove extraneous quotations around a string value.
if ("'""".Contains($val.Substring(0, 1))) {
$val = $val.Substring(1, $val.Length - 2)
}
$pyvenvConfig[$keyval[0]] = $val
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
}
}
}
return $pyvenvConfig
}
<# Begin Activate script --------------------------------------------------- #>
# Determine the containing directory of this script
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
$VenvExecDir = Get-Item -Path $VenvExecPath
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
# Set values required in priority: CmdLine, ConfigFile, Default
# First, get the location of the virtual environment, it might not be
# VenvExecDir if specified on the command line.
if ($VenvDir) {
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
}
else {
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
Write-Verbose "VenvDir=$VenvDir"
}
# Next, read the `pyvenv.cfg` file to determine any required value such
# as `prompt`.
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
# Next, set the prompt from the command line, or the config file, or
# just use the name of the virtual environment folder.
if ($Prompt) {
Write-Verbose "Prompt specified as argument, using '$Prompt'"
}
else {
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
$Prompt = $pyvenvCfg['prompt'];
}
else {
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
$Prompt = Split-Path -Path $venvDir -Leaf
}
}
Write-Verbose "Prompt = '$Prompt'"
Write-Verbose "VenvDir='$VenvDir'"
# Deactivate any currently active virtual environment, but leave the
# deactivate function in place.
deactivate -nondestructive
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
# that there is an activated venv.
$env:VIRTUAL_ENV = $VenvDir
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
Write-Verbose "Setting prompt to '$Prompt'"
# Set the prompt to include the env name
# Make sure _OLD_VIRTUAL_PROMPT is global
function global:_OLD_VIRTUAL_PROMPT { "" }
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
function global:prompt {
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
_OLD_VIRTUAL_PROMPT
}
$env:VIRTUAL_ENV_PROMPT = $Prompt
}
# Clear PYTHONHOME
if (Test-Path -Path Env:PYTHONHOME) {
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
Remove-Item -Path Env:PYTHONHOME
}
# Add the venv to the PATH
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"

70
Backend/venv/bin/activate Normal file
View File

@@ -0,0 +1,70 @@
# This file must be used with "source bin/activate" *from bash*
# You cannot run it directly
deactivate () {
# reset old environment variables
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
PATH="${_OLD_VIRTUAL_PATH:-}"
export PATH
unset _OLD_VIRTUAL_PATH
fi
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
export PYTHONHOME
unset _OLD_VIRTUAL_PYTHONHOME
fi
# Call hash to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
hash -r 2> /dev/null
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
PS1="${_OLD_VIRTUAL_PS1:-}"
export PS1
unset _OLD_VIRTUAL_PS1
fi
unset VIRTUAL_ENV
unset VIRTUAL_ENV_PROMPT
if [ ! "${1:-}" = "nondestructive" ] ; then
# Self destruct!
unset -f deactivate
fi
}
# unset irrelevant variables
deactivate nondestructive
# on Windows, a path can contain colons and backslashes and has to be converted:
if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then
# transform D:\path\to\venv to /d/path/to/venv on MSYS
# and to /cygdrive/d/path/to/venv on Cygwin
export VIRTUAL_ENV=$(cygpath /home/gnx/Desktop/Hotel-Booking/Backend/venv)
else
# use the path as-is
export VIRTUAL_ENV=/home/gnx/Desktop/Hotel-Booking/Backend/venv
fi
_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/"bin":$PATH"
export PATH
# unset PYTHONHOME if set
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
# could use `if (set -u; : $PYTHONHOME) ;` in bash
if [ -n "${PYTHONHOME:-}" ] ; then
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
unset PYTHONHOME
fi
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
_OLD_VIRTUAL_PS1="${PS1:-}"
PS1='(venv) '"${PS1:-}"
export PS1
VIRTUAL_ENV_PROMPT='(venv) '
export VIRTUAL_ENV_PROMPT
fi
# Call hash to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
hash -r 2> /dev/null

View File

@@ -0,0 +1,27 @@
# This file must be used with "source bin/activate.csh" *from csh*.
# You cannot run it directly.
# Created by Davide Di Blasi <davidedb@gmail.com>.
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
# Unset irrelevant variables.
deactivate nondestructive
setenv VIRTUAL_ENV /home/gnx/Desktop/Hotel-Booking/Backend/venv
set _OLD_VIRTUAL_PATH="$PATH"
setenv PATH "$VIRTUAL_ENV/"bin":$PATH"
set _OLD_VIRTUAL_PROMPT="$prompt"
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
set prompt = '(venv) '"$prompt"
setenv VIRTUAL_ENV_PROMPT '(venv) '
endif
alias pydoc python -m pydoc
rehash

View File

@@ -0,0 +1,69 @@
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
# (https://fishshell.com/). You cannot run it directly.
function deactivate -d "Exit virtual environment and return to normal shell environment"
# reset old environment variables
if test -n "$_OLD_VIRTUAL_PATH"
set -gx PATH $_OLD_VIRTUAL_PATH
set -e _OLD_VIRTUAL_PATH
end
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
set -e _OLD_VIRTUAL_PYTHONHOME
end
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
set -e _OLD_FISH_PROMPT_OVERRIDE
# prevents error when using nested fish instances (Issue #93858)
if functions -q _old_fish_prompt
functions -e fish_prompt
functions -c _old_fish_prompt fish_prompt
functions -e _old_fish_prompt
end
end
set -e VIRTUAL_ENV
set -e VIRTUAL_ENV_PROMPT
if test "$argv[1]" != "nondestructive"
# Self-destruct!
functions -e deactivate
end
end
# Unset irrelevant variables.
deactivate nondestructive
set -gx VIRTUAL_ENV /home/gnx/Desktop/Hotel-Booking/Backend/venv
set -gx _OLD_VIRTUAL_PATH $PATH
set -gx PATH "$VIRTUAL_ENV/"bin $PATH
# Unset PYTHONHOME if set.
if set -q PYTHONHOME
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
set -e PYTHONHOME
end
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
# fish uses a function instead of an env var to generate the prompt.
# Save the current fish_prompt function as the function _old_fish_prompt.
functions -c fish_prompt _old_fish_prompt
# With the original prompt function renamed, we can override with our own.
function fish_prompt
# Save the return status of the last command.
set -l old_status $status
# Output the venv prompt; color taken from the blue of the Python logo.
printf "%s%s%s" (set_color 4B8BBE) '(venv) ' (set_color normal)
# Restore the return status of the previous command.
echo "exit $old_status" | .
# Output the original/"old" prompt.
_old_fish_prompt
end
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
set -gx VIRTUAL_ENV_PROMPT '(venv) '
end

7
Backend/venv/bin/alembic Executable file
View File

@@ -0,0 +1,7 @@
#!/home/gnx/Desktop/Hotel-Booking/Backend/venv/bin/python3
import sys
from alembic.config import main
if __name__ == '__main__':
if sys.argv[0].endswith('.exe'):
sys.argv[0] = sys.argv[0][:-4]
sys.exit(main())

7
Backend/venv/bin/dotenv Executable file
View File

@@ -0,0 +1,7 @@
#!/home/gnx/Desktop/Hotel-Booking/Backend/venv/bin/python3
import sys
from dotenv.__main__ import cli
if __name__ == '__main__':
if sys.argv[0].endswith('.exe'):
sys.argv[0] = sys.argv[0][:-4]
sys.exit(cli())

View File

@@ -0,0 +1,7 @@
#!/home/gnx/Desktop/Hotel-Booking/Backend/venv/bin/python3
import sys
from email_validator.__main__ import main
if __name__ == '__main__':
if sys.argv[0].endswith('.exe'):
sys.argv[0] = sys.argv[0][:-4]
sys.exit(main())

7
Backend/venv/bin/mako-render Executable file
View File

@@ -0,0 +1,7 @@
#!/home/gnx/Desktop/Hotel-Booking/Backend/venv/bin/python3
import sys
from mako.cmd import cmdline
if __name__ == '__main__':
if sys.argv[0].endswith('.exe'):
sys.argv[0] = sys.argv[0][:-4]
sys.exit(cmdline())

8
Backend/venv/bin/pip Executable file
View File

@@ -0,0 +1,8 @@
#!/home/gnx/Desktop/Hotel-Booking/Backend/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

8
Backend/venv/bin/pip3 Executable file
View File

@@ -0,0 +1,8 @@
#!/home/gnx/Desktop/Hotel-Booking/Backend/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

8
Backend/venv/bin/pip3.12 Executable file
View File

@@ -0,0 +1,8 @@
#!/home/gnx/Desktop/Hotel-Booking/Backend/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

7
Backend/venv/bin/pyrsa-decrypt Executable file
View File

@@ -0,0 +1,7 @@
#!/home/gnx/Desktop/Hotel-Booking/Backend/venv/bin/python3
import sys
from rsa.cli import decrypt
if __name__ == '__main__':
if sys.argv[0].endswith('.exe'):
sys.argv[0] = sys.argv[0][:-4]
sys.exit(decrypt())

7
Backend/venv/bin/pyrsa-encrypt Executable file
View File

@@ -0,0 +1,7 @@
#!/home/gnx/Desktop/Hotel-Booking/Backend/venv/bin/python3
import sys
from rsa.cli import encrypt
if __name__ == '__main__':
if sys.argv[0].endswith('.exe'):
sys.argv[0] = sys.argv[0][:-4]
sys.exit(encrypt())

7
Backend/venv/bin/pyrsa-keygen Executable file
View File

@@ -0,0 +1,7 @@
#!/home/gnx/Desktop/Hotel-Booking/Backend/venv/bin/python3
import sys
from rsa.cli import keygen
if __name__ == '__main__':
if sys.argv[0].endswith('.exe'):
sys.argv[0] = sys.argv[0][:-4]
sys.exit(keygen())

View File

@@ -0,0 +1,7 @@
#!/home/gnx/Desktop/Hotel-Booking/Backend/venv/bin/python3
import sys
from rsa.util import private_to_public
if __name__ == '__main__':
if sys.argv[0].endswith('.exe'):
sys.argv[0] = sys.argv[0][:-4]
sys.exit(private_to_public())

7
Backend/venv/bin/pyrsa-sign Executable file
View File

@@ -0,0 +1,7 @@
#!/home/gnx/Desktop/Hotel-Booking/Backend/venv/bin/python3
import sys
from rsa.cli import sign
if __name__ == '__main__':
if sys.argv[0].endswith('.exe'):
sys.argv[0] = sys.argv[0][:-4]
sys.exit(sign())

7
Backend/venv/bin/pyrsa-verify Executable file
View File

@@ -0,0 +1,7 @@
#!/home/gnx/Desktop/Hotel-Booking/Backend/venv/bin/python3
import sys
from rsa.cli import verify
if __name__ == '__main__':
if sys.argv[0].endswith('.exe'):
sys.argv[0] = sys.argv[0][:-4]
sys.exit(verify())

Some files were not shown because too many files have changed in this diff Show More