updates
This commit is contained in:
@@ -6,65 +6,31 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
# Load environment variables
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
# Add parent directory to path
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||||
|
|
||||||
# Import models and Base
|
|
||||||
from src.config.database import Base
|
from src.config.database import Base
|
||||||
from src.config.settings import settings
|
from src.config.settings import settings
|
||||||
from src.models import * # Import all models
|
from src.models import *
|
||||||
|
|
||||||
# this is the Alembic Config object
|
|
||||||
config = context.config
|
config = context.config
|
||||||
|
|
||||||
# Interpret the config file for Python logging.
|
|
||||||
if config.config_file_name is not None:
|
if config.config_file_name is not None:
|
||||||
fileConfig(config.config_file_name)
|
fileConfig(config.config_file_name)
|
||||||
|
|
||||||
# Get database URL from settings
|
|
||||||
database_url = settings.database_url
|
database_url = settings.database_url
|
||||||
config.set_main_option("sqlalchemy.url", database_url)
|
config.set_main_option('sqlalchemy.url', database_url)
|
||||||
|
|
||||||
# add your model's MetaData object here
|
|
||||||
target_metadata = Base.metadata
|
target_metadata = Base.metadata
|
||||||
|
|
||||||
def run_migrations_offline() -> None:
|
def run_migrations_offline() -> None:
|
||||||
"""Run migrations in 'offline' mode."""
|
url = config.get_main_option('sqlalchemy.url')
|
||||||
url = config.get_main_option("sqlalchemy.url")
|
context.configure(url=url, target_metadata=target_metadata, literal_binds=True, dialect_opts={'paramstyle': 'named'})
|
||||||
context.configure(
|
|
||||||
url=url,
|
|
||||||
target_metadata=target_metadata,
|
|
||||||
literal_binds=True,
|
|
||||||
dialect_opts={"paramstyle": "named"},
|
|
||||||
)
|
|
||||||
|
|
||||||
with context.begin_transaction():
|
with context.begin_transaction():
|
||||||
context.run_migrations()
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
def run_migrations_online() -> None:
|
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)
|
||||||
connectable = engine_from_config(
|
|
||||||
config.get_section(config.config_ini_section, {}),
|
|
||||||
prefix="sqlalchemy.",
|
|
||||||
poolclass=pool.NullPool,
|
|
||||||
)
|
|
||||||
|
|
||||||
with connectable.connect() as connection:
|
with connectable.connect() as connection:
|
||||||
context.configure(
|
context.configure(connection=connection, target_metadata=target_metadata)
|
||||||
connection=connection, target_metadata=target_metadata
|
|
||||||
)
|
|
||||||
|
|
||||||
with context.begin_transaction():
|
with context.begin_transaction():
|
||||||
context.run_migrations()
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
if context.is_offline_mode():
|
if context.is_offline_mode():
|
||||||
run_migrations_offline()
|
run_migrations_offline()
|
||||||
else:
|
else:
|
||||||
run_migrations_online()
|
run_migrations_online()
|
||||||
|
|
||||||
@@ -1,31 +1,16 @@
|
|||||||
"""add_mfa_fields_to_users
|
|
||||||
|
|
||||||
Revision ID: 08e2f866e131
|
|
||||||
Revises: add_badges_to_page_content
|
|
||||||
Create Date: 2025-11-19 11:13:30.376194
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '08e2f866e131'
|
revision = '08e2f866e131'
|
||||||
down_revision = 'add_badges_to_page_content'
|
down_revision = 'add_badges_to_page_content'
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
# Add MFA fields to users table
|
|
||||||
op.add_column('users', sa.Column('mfa_enabled', sa.Boolean(), nullable=False, server_default='0'))
|
op.add_column('users', sa.Column('mfa_enabled', sa.Boolean(), nullable=False, server_default='0'))
|
||||||
op.add_column('users', sa.Column('mfa_secret', sa.String(255), nullable=True))
|
op.add_column('users', sa.Column('mfa_secret', sa.String(255), nullable=True))
|
||||||
op.add_column('users', sa.Column('mfa_backup_codes', sa.Text(), nullable=True))
|
op.add_column('users', sa.Column('mfa_backup_codes', sa.Text(), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
# Remove MFA fields from users table
|
|
||||||
op.drop_column('users', 'mfa_backup_codes')
|
op.drop_column('users', 'mfa_backup_codes')
|
||||||
op.drop_column('users', 'mfa_secret')
|
op.drop_column('users', 'mfa_secret')
|
||||||
op.drop_column('users', 'mfa_enabled')
|
op.drop_column('users', 'mfa_enabled')
|
||||||
|
|
||||||
@@ -1,23 +1,12 @@
|
|||||||
"""add_section_title_fields_to_page_content
|
|
||||||
|
|
||||||
Revision ID: 1444eb61188e
|
|
||||||
Revises: ff515d77abbe
|
|
||||||
Create Date: 2025-11-20 15:51:29.671843
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy.dialects import mysql
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '1444eb61188e'
|
revision = '1444eb61188e'
|
||||||
down_revision = 'ff515d77abbe'
|
down_revision = 'ff515d77abbe'
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.drop_index('bookings_room_id', table_name='bookings')
|
op.drop_index('bookings_room_id', table_name='bookings')
|
||||||
op.drop_index('bookings_status', table_name='bookings')
|
op.drop_index('bookings_status', table_name='bookings')
|
||||||
op.drop_index('bookings_user_id', table_name='bookings')
|
op.drop_index('bookings_user_id', table_name='bookings')
|
||||||
@@ -57,10 +46,7 @@ def upgrade() -> None:
|
|||||||
op.add_column('page_contents', sa.Column('awards_section_subtitle', sa.Text(), nullable=True))
|
op.add_column('page_contents', sa.Column('awards_section_subtitle', sa.Text(), nullable=True))
|
||||||
op.add_column('page_contents', sa.Column('partners_section_title', sa.Text(), nullable=True))
|
op.add_column('page_contents', sa.Column('partners_section_title', sa.Text(), nullable=True))
|
||||||
op.add_column('page_contents', sa.Column('partners_section_subtitle', sa.Text(), nullable=True))
|
op.add_column('page_contents', sa.Column('partners_section_subtitle', sa.Text(), nullable=True))
|
||||||
op.alter_column('password_reset_tokens', 'used',
|
op.alter_column('password_reset_tokens', 'used', existing_type=mysql.TINYINT(display_width=1), nullable=False, existing_server_default=sa.text("'0'"))
|
||||||
existing_type=mysql.TINYINT(display_width=1),
|
|
||||||
nullable=False,
|
|
||||||
existing_server_default=sa.text("'0'"))
|
|
||||||
op.drop_index('password_reset_tokens_token', table_name='password_reset_tokens')
|
op.drop_index('password_reset_tokens_token', table_name='password_reset_tokens')
|
||||||
op.drop_index('password_reset_tokens_user_id', table_name='password_reset_tokens')
|
op.drop_index('password_reset_tokens_user_id', table_name='password_reset_tokens')
|
||||||
op.drop_index('token', table_name='password_reset_tokens')
|
op.drop_index('token', table_name='password_reset_tokens')
|
||||||
@@ -68,11 +54,7 @@ def upgrade() -> None:
|
|||||||
op.create_index(op.f('ix_password_reset_tokens_token'), 'password_reset_tokens', ['token'], unique=True)
|
op.create_index(op.f('ix_password_reset_tokens_token'), 'password_reset_tokens', ['token'], unique=True)
|
||||||
op.drop_constraint('password_reset_tokens_ibfk_1', 'password_reset_tokens', type_='foreignkey')
|
op.drop_constraint('password_reset_tokens_ibfk_1', 'password_reset_tokens', type_='foreignkey')
|
||||||
op.create_foreign_key(None, 'password_reset_tokens', 'users', ['user_id'], ['id'])
|
op.create_foreign_key(None, 'password_reset_tokens', 'users', ['user_id'], ['id'])
|
||||||
op.alter_column('payments', 'deposit_percentage',
|
op.alter_column('payments', 'deposit_percentage', existing_type=mysql.INTEGER(), comment=None, existing_comment='Percentage of deposit (e.g., 20, 30, 50)', existing_nullable=True)
|
||||||
existing_type=mysql.INTEGER(),
|
|
||||||
comment=None,
|
|
||||||
existing_comment='Percentage of deposit (e.g., 20, 30, 50)',
|
|
||||||
existing_nullable=True)
|
|
||||||
op.drop_index('payments_booking_id', table_name='payments')
|
op.drop_index('payments_booking_id', table_name='payments')
|
||||||
op.drop_index('payments_payment_status', table_name='payments')
|
op.drop_index('payments_payment_status', table_name='payments')
|
||||||
op.create_index(op.f('ix_payments_id'), 'payments', ['id'], unique=False)
|
op.create_index(op.f('ix_payments_id'), 'payments', ['id'], unique=False)
|
||||||
@@ -128,11 +110,8 @@ def upgrade() -> None:
|
|||||||
op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)
|
op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)
|
||||||
op.drop_constraint('users_ibfk_1', 'users', type_='foreignkey')
|
op.drop_constraint('users_ibfk_1', 'users', type_='foreignkey')
|
||||||
op.create_foreign_key(None, 'users', 'roles', ['role_id'], ['id'])
|
op.create_foreign_key(None, 'users', 'roles', ['role_id'], ['id'])
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.drop_constraint(None, 'users', type_='foreignkey')
|
op.drop_constraint(None, 'users', type_='foreignkey')
|
||||||
op.create_foreign_key('users_ibfk_1', 'users', 'roles', ['role_id'], ['id'], onupdate='CASCADE', ondelete='RESTRICT')
|
op.create_foreign_key('users_ibfk_1', 'users', 'roles', ['role_id'], ['id'], onupdate='CASCADE', ondelete='RESTRICT')
|
||||||
op.drop_index(op.f('ix_users_id'), table_name='users')
|
op.drop_index(op.f('ix_users_id'), table_name='users')
|
||||||
@@ -188,10 +167,7 @@ def downgrade() -> None:
|
|||||||
op.drop_index(op.f('ix_payments_id'), table_name='payments')
|
op.drop_index(op.f('ix_payments_id'), table_name='payments')
|
||||||
op.create_index('payments_payment_status', 'payments', ['payment_status'], unique=False)
|
op.create_index('payments_payment_status', 'payments', ['payment_status'], unique=False)
|
||||||
op.create_index('payments_booking_id', 'payments', ['booking_id'], unique=False)
|
op.create_index('payments_booking_id', 'payments', ['booking_id'], unique=False)
|
||||||
op.alter_column('payments', 'deposit_percentage',
|
op.alter_column('payments', 'deposit_percentage', existing_type=mysql.INTEGER(), comment='Percentage of deposit (e.g., 20, 30, 50)', existing_nullable=True)
|
||||||
existing_type=mysql.INTEGER(),
|
|
||||||
comment='Percentage of deposit (e.g., 20, 30, 50)',
|
|
||||||
existing_nullable=True)
|
|
||||||
op.drop_constraint(None, 'password_reset_tokens', type_='foreignkey')
|
op.drop_constraint(None, 'password_reset_tokens', type_='foreignkey')
|
||||||
op.create_foreign_key('password_reset_tokens_ibfk_1', 'password_reset_tokens', 'users', ['user_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE')
|
op.create_foreign_key('password_reset_tokens_ibfk_1', 'password_reset_tokens', 'users', ['user_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE')
|
||||||
op.drop_index(op.f('ix_password_reset_tokens_token'), table_name='password_reset_tokens')
|
op.drop_index(op.f('ix_password_reset_tokens_token'), table_name='password_reset_tokens')
|
||||||
@@ -199,10 +175,7 @@ def downgrade() -> None:
|
|||||||
op.create_index('token', 'password_reset_tokens', ['token'], unique=False)
|
op.create_index('token', 'password_reset_tokens', ['token'], unique=False)
|
||||||
op.create_index('password_reset_tokens_user_id', 'password_reset_tokens', ['user_id'], unique=False)
|
op.create_index('password_reset_tokens_user_id', 'password_reset_tokens', ['user_id'], unique=False)
|
||||||
op.create_index('password_reset_tokens_token', 'password_reset_tokens', ['token'], unique=False)
|
op.create_index('password_reset_tokens_token', 'password_reset_tokens', ['token'], unique=False)
|
||||||
op.alter_column('password_reset_tokens', 'used',
|
op.alter_column('password_reset_tokens', 'used', existing_type=mysql.TINYINT(display_width=1), nullable=True, existing_server_default=sa.text("'0'"))
|
||||||
existing_type=mysql.TINYINT(display_width=1),
|
|
||||||
nullable=True,
|
|
||||||
existing_server_default=sa.text("'0'"))
|
|
||||||
op.drop_column('page_contents', 'partners_section_subtitle')
|
op.drop_column('page_contents', 'partners_section_subtitle')
|
||||||
op.drop_column('page_contents', 'partners_section_title')
|
op.drop_column('page_contents', 'partners_section_title')
|
||||||
op.drop_column('page_contents', 'awards_section_subtitle')
|
op.drop_column('page_contents', 'awards_section_subtitle')
|
||||||
@@ -241,6 +214,4 @@ def downgrade() -> None:
|
|||||||
op.create_index('ix_bookings_promotion_code', 'bookings', ['promotion_code'], unique=False)
|
op.create_index('ix_bookings_promotion_code', 'bookings', ['promotion_code'], unique=False)
|
||||||
op.create_index('bookings_user_id', 'bookings', ['user_id'], unique=False)
|
op.create_index('bookings_user_id', 'bookings', ['user_id'], unique=False)
|
||||||
op.create_index('bookings_status', 'bookings', ['status'], unique=False)
|
op.create_index('bookings_status', 'bookings', ['status'], unique=False)
|
||||||
op.create_index('bookings_room_id', 'bookings', ['room_id'], unique=False)
|
op.create_index('bookings_room_id', 'bookings', ['room_id'], unique=False)
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
@@ -1,62 +1,18 @@
|
|||||||
"""add_page_content_table
|
|
||||||
|
|
||||||
Revision ID: 163657e72e93
|
|
||||||
Revises: 6a126cc5b23c
|
|
||||||
Create Date: 2025-11-18 18:02:03.480951
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy.dialects import mysql
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '163657e72e93'
|
revision = '163657e72e93'
|
||||||
down_revision = '6a126cc5b23c'
|
down_revision = '6a126cc5b23c'
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
op.create_table('page_contents', sa.Column('id', sa.Integer(), nullable=False), sa.Column('page_type', sa.Enum('home', 'contact', 'about', 'footer', 'seo', name='pagetype'), nullable=False), sa.Column('title', sa.String(length=500), nullable=True), sa.Column('subtitle', sa.String(length=1000), nullable=True), sa.Column('description', sa.Text(), nullable=True), sa.Column('content', sa.Text(), nullable=True), sa.Column('meta_title', sa.String(length=500), nullable=True), sa.Column('meta_description', sa.Text(), nullable=True), sa.Column('meta_keywords', sa.String(length=1000), nullable=True), sa.Column('og_title', sa.String(length=500), nullable=True), sa.Column('og_description', sa.Text(), nullable=True), sa.Column('og_image', sa.String(length=1000), nullable=True), sa.Column('canonical_url', sa.String(length=1000), nullable=True), sa.Column('contact_info', sa.Text(), nullable=True), sa.Column('social_links', sa.Text(), nullable=True), sa.Column('footer_links', sa.Text(), nullable=True), sa.Column('hero_title', sa.String(length=500), nullable=True), sa.Column('hero_subtitle', sa.String(length=1000), nullable=True), sa.Column('hero_image', sa.String(length=1000), nullable=True), sa.Column('story_content', sa.Text(), nullable=True), sa.Column('values', sa.Text(), nullable=True), sa.Column('features', sa.Text(), nullable=True), sa.Column('is_active', sa.Boolean(), nullable=False), sa.Column('created_at', sa.DateTime(), nullable=False), sa.Column('updated_at', sa.DateTime(), nullable=False), sa.PrimaryKeyConstraint('id'))
|
||||||
# Only create the page_contents table, skip other schema changes
|
|
||||||
op.create_table('page_contents',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('page_type', sa.Enum('home', 'contact', 'about', 'footer', 'seo', name='pagetype'), nullable=False),
|
|
||||||
sa.Column('title', sa.String(length=500), nullable=True),
|
|
||||||
sa.Column('subtitle', sa.String(length=1000), nullable=True),
|
|
||||||
sa.Column('description', sa.Text(), nullable=True),
|
|
||||||
sa.Column('content', sa.Text(), nullable=True),
|
|
||||||
sa.Column('meta_title', sa.String(length=500), nullable=True),
|
|
||||||
sa.Column('meta_description', sa.Text(), nullable=True),
|
|
||||||
sa.Column('meta_keywords', sa.String(length=1000), nullable=True),
|
|
||||||
sa.Column('og_title', sa.String(length=500), nullable=True),
|
|
||||||
sa.Column('og_description', sa.Text(), nullable=True),
|
|
||||||
sa.Column('og_image', sa.String(length=1000), nullable=True),
|
|
||||||
sa.Column('canonical_url', sa.String(length=1000), nullable=True),
|
|
||||||
sa.Column('contact_info', sa.Text(), nullable=True),
|
|
||||||
sa.Column('social_links', sa.Text(), nullable=True),
|
|
||||||
sa.Column('footer_links', sa.Text(), nullable=True),
|
|
||||||
sa.Column('hero_title', sa.String(length=500), nullable=True),
|
|
||||||
sa.Column('hero_subtitle', sa.String(length=1000), nullable=True),
|
|
||||||
sa.Column('hero_image', sa.String(length=1000), nullable=True),
|
|
||||||
sa.Column('story_content', sa.Text(), nullable=True),
|
|
||||||
sa.Column('values', sa.Text(), nullable=True),
|
|
||||||
sa.Column('features', sa.Text(), nullable=True),
|
|
||||||
sa.Column('is_active', sa.Boolean(), nullable=False),
|
|
||||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
|
||||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('id')
|
|
||||||
)
|
|
||||||
op.create_index(op.f('ix_page_contents_id'), 'page_contents', ['id'], unique=False)
|
op.create_index(op.f('ix_page_contents_id'), 'page_contents', ['id'], unique=False)
|
||||||
op.create_index(op.f('ix_page_contents_page_type'), 'page_contents', ['page_type'], unique=True)
|
op.create_index(op.f('ix_page_contents_page_type'), 'page_contents', ['page_type'], unique=True)
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.drop_index(op.f('ix_page_contents_page_type'), table_name='page_contents')
|
op.drop_index(op.f('ix_page_contents_page_type'), table_name='page_contents')
|
||||||
op.drop_index(op.f('ix_page_contents_id'), table_name='page_contents')
|
op.drop_index(op.f('ix_page_contents_id'), table_name='page_contents')
|
||||||
op.drop_table('page_contents')
|
op.drop_table('page_contents')
|
||||||
op.execute("DROP TYPE IF EXISTS pagetype")
|
op.execute('DROP TYPE IF EXISTS pagetype')
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
@@ -1,38 +1,22 @@
|
|||||||
"""add_luxury_section_fields_to_page_content
|
|
||||||
|
|
||||||
Revision ID: 17efc6439cc3
|
|
||||||
Revises: bfa74be4b256
|
|
||||||
Create Date: 2025-11-20 13:37:20.015422
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '17efc6439cc3'
|
revision = '17efc6439cc3'
|
||||||
down_revision = 'bfa74be4b256'
|
down_revision = 'bfa74be4b256'
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
# Add luxury section fields to page_contents table
|
|
||||||
# Use TEXT instead of VARCHAR to avoid MySQL row size limits
|
|
||||||
op.add_column('page_contents', sa.Column('luxury_section_title', sa.Text(), nullable=True))
|
op.add_column('page_contents', sa.Column('luxury_section_title', sa.Text(), nullable=True))
|
||||||
op.add_column('page_contents', sa.Column('luxury_section_subtitle', sa.Text(), nullable=True))
|
op.add_column('page_contents', sa.Column('luxury_section_subtitle', sa.Text(), nullable=True))
|
||||||
op.add_column('page_contents', sa.Column('luxury_section_image', sa.Text(), nullable=True))
|
op.add_column('page_contents', sa.Column('luxury_section_image', sa.Text(), nullable=True))
|
||||||
op.add_column('page_contents', sa.Column('luxury_features', sa.Text(), nullable=True)) # JSON array
|
op.add_column('page_contents', sa.Column('luxury_features', sa.Text(), nullable=True))
|
||||||
op.add_column('page_contents', sa.Column('luxury_gallery', sa.Text(), nullable=True)) # JSON array of image URLs
|
op.add_column('page_contents', sa.Column('luxury_gallery', sa.Text(), nullable=True))
|
||||||
op.add_column('page_contents', sa.Column('luxury_testimonials', sa.Text(), nullable=True)) # JSON array of testimonials
|
op.add_column('page_contents', sa.Column('luxury_testimonials', sa.Text(), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
# Remove luxury section fields
|
|
||||||
op.drop_column('page_contents', 'luxury_testimonials')
|
op.drop_column('page_contents', 'luxury_testimonials')
|
||||||
op.drop_column('page_contents', 'luxury_gallery')
|
op.drop_column('page_contents', 'luxury_gallery')
|
||||||
op.drop_column('page_contents', 'luxury_features')
|
op.drop_column('page_contents', 'luxury_features')
|
||||||
op.drop_column('page_contents', 'luxury_section_image')
|
op.drop_column('page_contents', 'luxury_section_image')
|
||||||
op.drop_column('page_contents', 'luxury_section_subtitle')
|
op.drop_column('page_contents', 'luxury_section_subtitle')
|
||||||
op.drop_column('page_contents', 'luxury_section_title')
|
op.drop_column('page_contents', 'luxury_section_title')
|
||||||
|
|
||||||
@@ -1,39 +1,13 @@
|
|||||||
"""Initial migration: create all tables with indexes
|
|
||||||
|
|
||||||
Revision ID: 59baf2338f8a
|
|
||||||
Revises:
|
|
||||||
Create Date: 2025-11-16 16:03:26.313117
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy.dialects import mysql
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '59baf2338f8a'
|
revision = '59baf2338f8a'
|
||||||
down_revision = None
|
down_revision = None
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
op.create_table('audit_logs', sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), sa.Column('user_id', sa.Integer(), nullable=True), sa.Column('action', sa.String(length=100), nullable=False), sa.Column('resource_type', sa.String(length=50), nullable=False), sa.Column('resource_id', sa.Integer(), nullable=True), sa.Column('ip_address', sa.String(length=45), nullable=True), sa.Column('user_agent', sa.String(length=255), nullable=True), sa.Column('request_id', sa.String(length=36), nullable=True), sa.Column('details', sa.JSON(), nullable=True), sa.Column('status', sa.String(length=20), nullable=False), sa.Column('error_message', sa.Text(), nullable=True), sa.Column('created_at', sa.DateTime(), nullable=False), sa.ForeignKeyConstraint(['user_id'], ['users.id']), sa.PrimaryKeyConstraint('id'))
|
||||||
op.create_table('audit_logs',
|
|
||||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
|
||||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
|
||||||
sa.Column('action', sa.String(length=100), nullable=False),
|
|
||||||
sa.Column('resource_type', sa.String(length=50), nullable=False),
|
|
||||||
sa.Column('resource_id', sa.Integer(), nullable=True),
|
|
||||||
sa.Column('ip_address', sa.String(length=45), nullable=True),
|
|
||||||
sa.Column('user_agent', sa.String(length=255), nullable=True),
|
|
||||||
sa.Column('request_id', sa.String(length=36), nullable=True),
|
|
||||||
sa.Column('details', sa.JSON(), nullable=True),
|
|
||||||
sa.Column('status', sa.String(length=20), nullable=False),
|
|
||||||
sa.Column('error_message', sa.Text(), nullable=True),
|
|
||||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
|
||||||
sa.PrimaryKeyConstraint('id')
|
|
||||||
)
|
|
||||||
op.create_index(op.f('ix_audit_logs_action'), 'audit_logs', ['action'], unique=False)
|
op.create_index(op.f('ix_audit_logs_action'), 'audit_logs', ['action'], unique=False)
|
||||||
op.create_index(op.f('ix_audit_logs_created_at'), 'audit_logs', ['created_at'], unique=False)
|
op.create_index(op.f('ix_audit_logs_created_at'), 'audit_logs', ['created_at'], unique=False)
|
||||||
op.create_index(op.f('ix_audit_logs_id'), 'audit_logs', ['id'], unique=False)
|
op.create_index(op.f('ix_audit_logs_id'), 'audit_logs', ['id'], unique=False)
|
||||||
@@ -46,7 +20,6 @@ def upgrade() -> None:
|
|||||||
op.drop_index('banners_is_active', table_name='banners')
|
op.drop_index('banners_is_active', table_name='banners')
|
||||||
op.drop_index('banners_position', table_name='banners')
|
op.drop_index('banners_position', table_name='banners')
|
||||||
op.create_index(op.f('ix_banners_id'), 'banners', ['id'], unique=False)
|
op.create_index(op.f('ix_banners_id'), 'banners', ['id'], unique=False)
|
||||||
# Drop foreign keys first, then indexes
|
|
||||||
op.drop_constraint('bookings_ibfk_2', 'bookings', type_='foreignkey')
|
op.drop_constraint('bookings_ibfk_2', 'bookings', type_='foreignkey')
|
||||||
op.drop_constraint('bookings_ibfk_1', 'bookings', type_='foreignkey')
|
op.drop_constraint('bookings_ibfk_1', 'bookings', type_='foreignkey')
|
||||||
op.drop_index('booking_number', table_name='bookings')
|
op.drop_index('booking_number', table_name='bookings')
|
||||||
@@ -60,7 +33,6 @@ def upgrade() -> None:
|
|||||||
op.create_index(op.f('ix_bookings_id'), 'bookings', ['id'], unique=False)
|
op.create_index(op.f('ix_bookings_id'), 'bookings', ['id'], unique=False)
|
||||||
op.create_foreign_key(None, 'bookings', 'users', ['user_id'], ['id'])
|
op.create_foreign_key(None, 'bookings', 'users', ['user_id'], ['id'])
|
||||||
op.create_foreign_key(None, 'bookings', 'rooms', ['room_id'], ['id'])
|
op.create_foreign_key(None, 'bookings', 'rooms', ['room_id'], ['id'])
|
||||||
# Drop foreign keys first, then indexes
|
|
||||||
op.drop_constraint('checkin_checkout_ibfk_1', 'checkin_checkout', type_='foreignkey')
|
op.drop_constraint('checkin_checkout_ibfk_1', 'checkin_checkout', type_='foreignkey')
|
||||||
op.drop_constraint('checkin_checkout_ibfk_2', 'checkin_checkout', type_='foreignkey')
|
op.drop_constraint('checkin_checkout_ibfk_2', 'checkin_checkout', type_='foreignkey')
|
||||||
op.drop_constraint('checkin_checkout_ibfk_3', 'checkin_checkout', type_='foreignkey')
|
op.drop_constraint('checkin_checkout_ibfk_3', 'checkin_checkout', type_='foreignkey')
|
||||||
@@ -70,7 +42,6 @@ def upgrade() -> None:
|
|||||||
op.create_foreign_key(None, 'checkin_checkout', 'bookings', ['booking_id'], ['id'])
|
op.create_foreign_key(None, 'checkin_checkout', 'bookings', ['booking_id'], ['id'])
|
||||||
op.create_foreign_key(None, 'checkin_checkout', 'users', ['checkout_by'], ['id'])
|
op.create_foreign_key(None, 'checkin_checkout', 'users', ['checkout_by'], ['id'])
|
||||||
op.create_foreign_key(None, 'checkin_checkout', 'users', ['checkin_by'], ['id'])
|
op.create_foreign_key(None, 'checkin_checkout', 'users', ['checkin_by'], ['id'])
|
||||||
# Drop foreign keys first, then indexes
|
|
||||||
op.drop_constraint('favorites_ibfk_2', 'favorites', type_='foreignkey')
|
op.drop_constraint('favorites_ibfk_2', 'favorites', type_='foreignkey')
|
||||||
op.drop_constraint('favorites_ibfk_1', 'favorites', type_='foreignkey')
|
op.drop_constraint('favorites_ibfk_1', 'favorites', type_='foreignkey')
|
||||||
op.drop_index('favorites_room_id', table_name='favorites')
|
op.drop_index('favorites_room_id', table_name='favorites')
|
||||||
@@ -79,11 +50,7 @@ def upgrade() -> None:
|
|||||||
op.create_index(op.f('ix_favorites_id'), 'favorites', ['id'], unique=False)
|
op.create_index(op.f('ix_favorites_id'), 'favorites', ['id'], unique=False)
|
||||||
op.create_foreign_key(None, 'favorites', 'users', ['user_id'], ['id'])
|
op.create_foreign_key(None, 'favorites', 'users', ['user_id'], ['id'])
|
||||||
op.create_foreign_key(None, 'favorites', 'rooms', ['room_id'], ['id'])
|
op.create_foreign_key(None, 'favorites', 'rooms', ['room_id'], ['id'])
|
||||||
op.alter_column('password_reset_tokens', 'used',
|
op.alter_column('password_reset_tokens', 'used', existing_type=mysql.TINYINT(display_width=1), nullable=False, existing_server_default=sa.text("'0'"))
|
||||||
existing_type=mysql.TINYINT(display_width=1),
|
|
||||||
nullable=False,
|
|
||||||
existing_server_default=sa.text("'0'"))
|
|
||||||
# Drop foreign key first, then indexes
|
|
||||||
op.drop_constraint('password_reset_tokens_ibfk_1', 'password_reset_tokens', type_='foreignkey')
|
op.drop_constraint('password_reset_tokens_ibfk_1', 'password_reset_tokens', type_='foreignkey')
|
||||||
op.drop_index('password_reset_tokens_token', table_name='password_reset_tokens')
|
op.drop_index('password_reset_tokens_token', table_name='password_reset_tokens')
|
||||||
op.drop_index('password_reset_tokens_user_id', table_name='password_reset_tokens')
|
op.drop_index('password_reset_tokens_user_id', table_name='password_reset_tokens')
|
||||||
@@ -91,12 +58,7 @@ def upgrade() -> None:
|
|||||||
op.create_index(op.f('ix_password_reset_tokens_id'), 'password_reset_tokens', ['id'], unique=False)
|
op.create_index(op.f('ix_password_reset_tokens_id'), 'password_reset_tokens', ['id'], unique=False)
|
||||||
op.create_index(op.f('ix_password_reset_tokens_token'), 'password_reset_tokens', ['token'], unique=True)
|
op.create_index(op.f('ix_password_reset_tokens_token'), 'password_reset_tokens', ['token'], unique=True)
|
||||||
op.create_foreign_key(None, 'password_reset_tokens', 'users', ['user_id'], ['id'])
|
op.create_foreign_key(None, 'password_reset_tokens', 'users', ['user_id'], ['id'])
|
||||||
op.alter_column('payments', 'deposit_percentage',
|
op.alter_column('payments', 'deposit_percentage', existing_type=mysql.INTEGER(), comment=None, existing_comment='Percentage of deposit (e.g., 20, 30, 50)', existing_nullable=True)
|
||||||
existing_type=mysql.INTEGER(),
|
|
||||||
comment=None,
|
|
||||||
existing_comment='Percentage of deposit (e.g., 20, 30, 50)',
|
|
||||||
existing_nullable=True)
|
|
||||||
# Drop foreign keys first, then indexes
|
|
||||||
op.drop_constraint('payments_related_payment_id_foreign_idx', 'payments', type_='foreignkey')
|
op.drop_constraint('payments_related_payment_id_foreign_idx', 'payments', type_='foreignkey')
|
||||||
op.drop_constraint('payments_ibfk_1', 'payments', type_='foreignkey')
|
op.drop_constraint('payments_ibfk_1', 'payments', type_='foreignkey')
|
||||||
op.drop_index('payments_booking_id', table_name='payments')
|
op.drop_index('payments_booking_id', table_name='payments')
|
||||||
@@ -109,7 +71,6 @@ def upgrade() -> None:
|
|||||||
op.drop_index('promotions_is_active', table_name='promotions')
|
op.drop_index('promotions_is_active', table_name='promotions')
|
||||||
op.create_index(op.f('ix_promotions_code'), 'promotions', ['code'], unique=True)
|
op.create_index(op.f('ix_promotions_code'), 'promotions', ['code'], unique=True)
|
||||||
op.create_index(op.f('ix_promotions_id'), 'promotions', ['id'], unique=False)
|
op.create_index(op.f('ix_promotions_id'), 'promotions', ['id'], unique=False)
|
||||||
# Drop foreign key first, then indexes
|
|
||||||
op.drop_constraint('refresh_tokens_ibfk_1', 'refresh_tokens', type_='foreignkey')
|
op.drop_constraint('refresh_tokens_ibfk_1', 'refresh_tokens', type_='foreignkey')
|
||||||
op.drop_index('refresh_tokens_token', table_name='refresh_tokens')
|
op.drop_index('refresh_tokens_token', table_name='refresh_tokens')
|
||||||
op.drop_index('refresh_tokens_user_id', table_name='refresh_tokens')
|
op.drop_index('refresh_tokens_user_id', table_name='refresh_tokens')
|
||||||
@@ -117,7 +78,6 @@ def upgrade() -> None:
|
|||||||
op.create_index(op.f('ix_refresh_tokens_id'), 'refresh_tokens', ['id'], unique=False)
|
op.create_index(op.f('ix_refresh_tokens_id'), 'refresh_tokens', ['id'], unique=False)
|
||||||
op.create_index(op.f('ix_refresh_tokens_token'), 'refresh_tokens', ['token'], unique=True)
|
op.create_index(op.f('ix_refresh_tokens_token'), 'refresh_tokens', ['token'], unique=True)
|
||||||
op.create_foreign_key(None, 'refresh_tokens', 'users', ['user_id'], ['id'])
|
op.create_foreign_key(None, 'refresh_tokens', 'users', ['user_id'], ['id'])
|
||||||
# Drop foreign keys first, then indexes
|
|
||||||
op.drop_constraint('reviews_ibfk_2', 'reviews', type_='foreignkey')
|
op.drop_constraint('reviews_ibfk_2', 'reviews', type_='foreignkey')
|
||||||
op.drop_constraint('reviews_ibfk_1', 'reviews', type_='foreignkey')
|
op.drop_constraint('reviews_ibfk_1', 'reviews', type_='foreignkey')
|
||||||
op.drop_index('reviews_room_id', table_name='reviews')
|
op.drop_index('reviews_room_id', table_name='reviews')
|
||||||
@@ -130,7 +90,6 @@ def upgrade() -> None:
|
|||||||
op.create_index(op.f('ix_roles_id'), 'roles', ['id'], unique=False)
|
op.create_index(op.f('ix_roles_id'), 'roles', ['id'], unique=False)
|
||||||
op.create_index(op.f('ix_roles_name'), 'roles', ['name'], unique=True)
|
op.create_index(op.f('ix_roles_name'), 'roles', ['name'], unique=True)
|
||||||
op.create_index(op.f('ix_room_types_id'), 'room_types', ['id'], unique=False)
|
op.create_index(op.f('ix_room_types_id'), 'room_types', ['id'], unique=False)
|
||||||
# Drop foreign key first, then indexes
|
|
||||||
op.drop_constraint('rooms_ibfk_1', 'rooms', type_='foreignkey')
|
op.drop_constraint('rooms_ibfk_1', 'rooms', type_='foreignkey')
|
||||||
op.drop_index('room_number', table_name='rooms')
|
op.drop_index('room_number', table_name='rooms')
|
||||||
op.drop_index('rooms_featured', table_name='rooms')
|
op.drop_index('rooms_featured', table_name='rooms')
|
||||||
@@ -139,7 +98,6 @@ def upgrade() -> None:
|
|||||||
op.create_index(op.f('ix_rooms_id'), 'rooms', ['id'], unique=False)
|
op.create_index(op.f('ix_rooms_id'), 'rooms', ['id'], unique=False)
|
||||||
op.create_index(op.f('ix_rooms_room_number'), 'rooms', ['room_number'], unique=True)
|
op.create_index(op.f('ix_rooms_room_number'), 'rooms', ['room_number'], unique=True)
|
||||||
op.create_foreign_key(None, 'rooms', 'room_types', ['room_type_id'], ['id'])
|
op.create_foreign_key(None, 'rooms', 'room_types', ['room_type_id'], ['id'])
|
||||||
# Drop foreign keys first, then indexes
|
|
||||||
op.drop_constraint('service_usages_ibfk_1', 'service_usages', type_='foreignkey')
|
op.drop_constraint('service_usages_ibfk_1', 'service_usages', type_='foreignkey')
|
||||||
op.drop_constraint('service_usages_ibfk_2', 'service_usages', type_='foreignkey')
|
op.drop_constraint('service_usages_ibfk_2', 'service_usages', type_='foreignkey')
|
||||||
op.drop_index('service_usages_booking_id', table_name='service_usages')
|
op.drop_index('service_usages_booking_id', table_name='service_usages')
|
||||||
@@ -149,7 +107,6 @@ def upgrade() -> None:
|
|||||||
op.create_foreign_key(None, 'service_usages', 'services', ['service_id'], ['id'])
|
op.create_foreign_key(None, 'service_usages', 'services', ['service_id'], ['id'])
|
||||||
op.drop_index('services_category', table_name='services')
|
op.drop_index('services_category', table_name='services')
|
||||||
op.create_index(op.f('ix_services_id'), 'services', ['id'], unique=False)
|
op.create_index(op.f('ix_services_id'), 'services', ['id'], unique=False)
|
||||||
# Drop foreign key first, then indexes
|
|
||||||
op.drop_constraint('users_ibfk_1', 'users', type_='foreignkey')
|
op.drop_constraint('users_ibfk_1', 'users', type_='foreignkey')
|
||||||
op.drop_index('email', table_name='users')
|
op.drop_index('email', table_name='users')
|
||||||
op.drop_index('users_email', table_name='users')
|
op.drop_index('users_email', table_name='users')
|
||||||
@@ -157,11 +114,8 @@ def upgrade() -> None:
|
|||||||
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
|
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
|
||||||
op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)
|
op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)
|
||||||
op.create_foreign_key(None, 'users', 'roles', ['role_id'], ['id'])
|
op.create_foreign_key(None, 'users', 'roles', ['role_id'], ['id'])
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.drop_constraint(None, 'users', type_='foreignkey')
|
op.drop_constraint(None, 'users', type_='foreignkey')
|
||||||
op.create_foreign_key('users_ibfk_1', 'users', 'roles', ['role_id'], ['id'], onupdate='CASCADE', ondelete='RESTRICT')
|
op.create_foreign_key('users_ibfk_1', 'users', 'roles', ['role_id'], ['id'], onupdate='CASCADE', ondelete='RESTRICT')
|
||||||
op.drop_index(op.f('ix_users_id'), table_name='users')
|
op.drop_index(op.f('ix_users_id'), table_name='users')
|
||||||
@@ -217,10 +171,7 @@ def downgrade() -> None:
|
|||||||
op.drop_index(op.f('ix_payments_id'), table_name='payments')
|
op.drop_index(op.f('ix_payments_id'), table_name='payments')
|
||||||
op.create_index('payments_payment_status', 'payments', ['payment_status'], unique=False)
|
op.create_index('payments_payment_status', 'payments', ['payment_status'], unique=False)
|
||||||
op.create_index('payments_booking_id', 'payments', ['booking_id'], unique=False)
|
op.create_index('payments_booking_id', 'payments', ['booking_id'], unique=False)
|
||||||
op.alter_column('payments', 'deposit_percentage',
|
op.alter_column('payments', 'deposit_percentage', existing_type=mysql.INTEGER(), comment='Percentage of deposit (e.g., 20, 30, 50)', existing_nullable=True)
|
||||||
existing_type=mysql.INTEGER(),
|
|
||||||
comment='Percentage of deposit (e.g., 20, 30, 50)',
|
|
||||||
existing_nullable=True)
|
|
||||||
op.drop_constraint(None, 'password_reset_tokens', type_='foreignkey')
|
op.drop_constraint(None, 'password_reset_tokens', type_='foreignkey')
|
||||||
op.create_foreign_key('password_reset_tokens_ibfk_1', 'password_reset_tokens', 'users', ['user_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE')
|
op.create_foreign_key('password_reset_tokens_ibfk_1', 'password_reset_tokens', 'users', ['user_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE')
|
||||||
op.drop_index(op.f('ix_password_reset_tokens_token'), table_name='password_reset_tokens')
|
op.drop_index(op.f('ix_password_reset_tokens_token'), table_name='password_reset_tokens')
|
||||||
@@ -228,10 +179,7 @@ def downgrade() -> None:
|
|||||||
op.create_index('token', 'password_reset_tokens', ['token'], unique=False)
|
op.create_index('token', 'password_reset_tokens', ['token'], unique=False)
|
||||||
op.create_index('password_reset_tokens_user_id', 'password_reset_tokens', ['user_id'], unique=False)
|
op.create_index('password_reset_tokens_user_id', 'password_reset_tokens', ['user_id'], unique=False)
|
||||||
op.create_index('password_reset_tokens_token', 'password_reset_tokens', ['token'], unique=False)
|
op.create_index('password_reset_tokens_token', 'password_reset_tokens', ['token'], unique=False)
|
||||||
op.alter_column('password_reset_tokens', 'used',
|
op.alter_column('password_reset_tokens', 'used', existing_type=mysql.TINYINT(display_width=1), nullable=True, existing_server_default=sa.text("'0'"))
|
||||||
existing_type=mysql.TINYINT(display_width=1),
|
|
||||||
nullable=True,
|
|
||||||
existing_server_default=sa.text("'0'"))
|
|
||||||
op.drop_constraint(None, 'favorites', type_='foreignkey')
|
op.drop_constraint(None, 'favorites', type_='foreignkey')
|
||||||
op.drop_constraint(None, 'favorites', type_='foreignkey')
|
op.drop_constraint(None, 'favorites', type_='foreignkey')
|
||||||
op.create_foreign_key('favorites_ibfk_1', 'favorites', 'users', ['user_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE')
|
op.create_foreign_key('favorites_ibfk_1', 'favorites', 'users', ['user_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE')
|
||||||
@@ -265,13 +213,7 @@ def downgrade() -> None:
|
|||||||
op.drop_index(op.f('ix_banners_id'), table_name='banners')
|
op.drop_index(op.f('ix_banners_id'), table_name='banners')
|
||||||
op.create_index('banners_position', 'banners', ['position'], unique=False)
|
op.create_index('banners_position', 'banners', ['position'], unique=False)
|
||||||
op.create_index('banners_is_active', 'banners', ['is_active'], unique=False)
|
op.create_index('banners_is_active', 'banners', ['is_active'], unique=False)
|
||||||
op.create_table('SequelizeMeta',
|
op.create_table('SequelizeMeta', sa.Column('name', mysql.VARCHAR(collation='utf8mb3_unicode_ci', length=255), nullable=False), sa.PrimaryKeyConstraint('name'), mysql_collate='utf8mb3_unicode_ci', mysql_default_charset='utf8mb3', mysql_engine='InnoDB')
|
||||||
sa.Column('name', mysql.VARCHAR(collation='utf8mb3_unicode_ci', length=255), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('name'),
|
|
||||||
mysql_collate='utf8mb3_unicode_ci',
|
|
||||||
mysql_default_charset='utf8mb3',
|
|
||||||
mysql_engine='InnoDB'
|
|
||||||
)
|
|
||||||
op.create_index('name', 'SequelizeMeta', ['name'], unique=False)
|
op.create_index('name', 'SequelizeMeta', ['name'], unique=False)
|
||||||
op.drop_index(op.f('ix_audit_logs_user_id'), table_name='audit_logs')
|
op.drop_index(op.f('ix_audit_logs_user_id'), table_name='audit_logs')
|
||||||
op.drop_index(op.f('ix_audit_logs_resource_type'), table_name='audit_logs')
|
op.drop_index(op.f('ix_audit_logs_resource_type'), table_name='audit_logs')
|
||||||
@@ -280,6 +222,4 @@ def downgrade() -> None:
|
|||||||
op.drop_index(op.f('ix_audit_logs_id'), table_name='audit_logs')
|
op.drop_index(op.f('ix_audit_logs_id'), table_name='audit_logs')
|
||||||
op.drop_index(op.f('ix_audit_logs_created_at'), table_name='audit_logs')
|
op.drop_index(op.f('ix_audit_logs_created_at'), table_name='audit_logs')
|
||||||
op.drop_index(op.f('ix_audit_logs_action'), table_name='audit_logs')
|
op.drop_index(op.f('ix_audit_logs_action'), table_name='audit_logs')
|
||||||
op.drop_table('audit_logs')
|
op.drop_table('audit_logs')
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
@@ -1,29 +1,16 @@
|
|||||||
"""add_capacity_room_size_view_to_rooms
|
|
||||||
|
|
||||||
Revision ID: 6a126cc5b23c
|
|
||||||
Revises: add_stripe_payment_method
|
|
||||||
Create Date: 2025-11-17 16:25:09.581786
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '6a126cc5b23c'
|
revision = '6a126cc5b23c'
|
||||||
down_revision = 'add_stripe_payment_method'
|
down_revision = 'add_stripe_payment_method'
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
# Add the three new columns to rooms table
|
|
||||||
op.add_column('rooms', sa.Column('capacity', sa.Integer(), nullable=True))
|
op.add_column('rooms', sa.Column('capacity', sa.Integer(), nullable=True))
|
||||||
op.add_column('rooms', sa.Column('room_size', sa.String(length=50), nullable=True))
|
op.add_column('rooms', sa.Column('room_size', sa.String(length=50), nullable=True))
|
||||||
op.add_column('rooms', sa.Column('view', sa.String(length=100), nullable=True))
|
op.add_column('rooms', sa.Column('view', sa.String(length=100), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
# Remove the three columns from rooms table
|
|
||||||
op.drop_column('rooms', 'view')
|
op.drop_column('rooms', 'view')
|
||||||
op.drop_column('rooms', 'room_size')
|
op.drop_column('rooms', 'room_size')
|
||||||
op.drop_column('rooms', 'capacity')
|
op.drop_column('rooms', 'capacity')
|
||||||
@@ -1,60 +1,29 @@
|
|||||||
"""add_system_settings_table
|
|
||||||
|
|
||||||
Revision ID: 96c23dad405d
|
|
||||||
Revises: 59baf2338f8a
|
|
||||||
Create Date: 2025-11-17 11:51:28.369031
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy.dialects import mysql
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = '96c23dad405d'
|
revision = '96c23dad405d'
|
||||||
down_revision = '59baf2338f8a'
|
down_revision = '59baf2338f8a'
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
# Create system_settings table (if it doesn't exist)
|
|
||||||
from sqlalchemy import inspect
|
from sqlalchemy import inspect
|
||||||
bind = op.get_bind()
|
bind = op.get_bind()
|
||||||
inspector = inspect(bind)
|
inspector = inspect(bind)
|
||||||
tables = inspector.get_table_names()
|
tables = inspector.get_table_names()
|
||||||
|
|
||||||
if 'system_settings' not in tables:
|
if 'system_settings' not in tables:
|
||||||
op.create_table('system_settings',
|
op.create_table('system_settings', sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), sa.Column('key', sa.String(length=100), nullable=False), sa.Column('value', sa.Text(), nullable=False), sa.Column('description', sa.Text(), nullable=True), sa.Column('updated_at', sa.DateTime(), nullable=False), sa.Column('updated_by_id', sa.Integer(), nullable=True), sa.ForeignKeyConstraint(['updated_by_id'], ['users.id']), sa.PrimaryKeyConstraint('id'))
|
||||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
|
||||||
sa.Column('key', sa.String(length=100), nullable=False),
|
|
||||||
sa.Column('value', sa.Text(), nullable=False),
|
|
||||||
sa.Column('description', sa.Text(), nullable=True),
|
|
||||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
|
||||||
sa.Column('updated_by_id', sa.Integer(), nullable=True),
|
|
||||||
sa.ForeignKeyConstraint(['updated_by_id'], ['users.id'], ),
|
|
||||||
sa.PrimaryKeyConstraint('id')
|
|
||||||
)
|
|
||||||
op.create_index(op.f('ix_system_settings_id'), 'system_settings', ['id'], unique=False)
|
op.create_index(op.f('ix_system_settings_id'), 'system_settings', ['id'], unique=False)
|
||||||
op.create_index(op.f('ix_system_settings_key'), 'system_settings', ['key'], unique=True)
|
op.create_index(op.f('ix_system_settings_key'), 'system_settings', ['key'], unique=True)
|
||||||
|
|
||||||
# Add currency column to users table (if it doesn't exist)
|
|
||||||
columns = [col['name'] for col in inspector.get_columns('users')]
|
columns = [col['name'] for col in inspector.get_columns('users')]
|
||||||
if 'currency' not in columns:
|
if 'currency' not in columns:
|
||||||
op.add_column('users', sa.Column('currency', sa.String(length=3), nullable=False, server_default='VND'))
|
op.add_column('users', sa.Column('currency', sa.String(length=3), nullable=False, server_default='VND'))
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
# Drop currency column from users table
|
|
||||||
try:
|
try:
|
||||||
op.drop_column('users', 'currency')
|
op.drop_column('users', 'currency')
|
||||||
except Exception:
|
except Exception:
|
||||||
# Column might not exist, skip
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Drop system_settings table
|
|
||||||
op.drop_index(op.f('ix_system_settings_key'), table_name='system_settings')
|
op.drop_index(op.f('ix_system_settings_key'), table_name='system_settings')
|
||||||
op.drop_index(op.f('ix_system_settings_id'), table_name='system_settings')
|
op.drop_index(op.f('ix_system_settings_id'), table_name='system_settings')
|
||||||
op.drop_table('system_settings')
|
op.drop_table('system_settings')
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
@@ -1,22 +1,11 @@
|
|||||||
"""add_about_page_fields
|
|
||||||
|
|
||||||
Revision ID: f2a3b4c5d6e7
|
|
||||||
Revises: a1b2c3d4e5f6
|
|
||||||
Create Date: 2025-11-20 17:00:00.000000
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'f2a3b4c5d6e7'
|
revision = 'f2a3b4c5d6e7'
|
||||||
down_revision = 'a1b2c3d4e5f6'
|
down_revision = 'a1b2c3d4e5f6'
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
# Add about page specific fields (all as TEXT to avoid row size issues)
|
|
||||||
op.add_column('page_contents', sa.Column('about_hero_image', sa.Text(), nullable=True))
|
op.add_column('page_contents', sa.Column('about_hero_image', sa.Text(), nullable=True))
|
||||||
op.add_column('page_contents', sa.Column('mission', sa.Text(), nullable=True))
|
op.add_column('page_contents', sa.Column('mission', sa.Text(), nullable=True))
|
||||||
op.add_column('page_contents', sa.Column('vision', sa.Text(), nullable=True))
|
op.add_column('page_contents', sa.Column('vision', sa.Text(), nullable=True))
|
||||||
@@ -24,13 +13,10 @@ def upgrade() -> None:
|
|||||||
op.add_column('page_contents', sa.Column('timeline', sa.Text(), nullable=True))
|
op.add_column('page_contents', sa.Column('timeline', sa.Text(), nullable=True))
|
||||||
op.add_column('page_contents', sa.Column('achievements', sa.Text(), nullable=True))
|
op.add_column('page_contents', sa.Column('achievements', sa.Text(), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
# Remove about page specific fields
|
|
||||||
op.drop_column('page_contents', 'achievements')
|
op.drop_column('page_contents', 'achievements')
|
||||||
op.drop_column('page_contents', 'timeline')
|
op.drop_column('page_contents', 'timeline')
|
||||||
op.drop_column('page_contents', 'team')
|
op.drop_column('page_contents', 'team')
|
||||||
op.drop_column('page_contents', 'vision')
|
op.drop_column('page_contents', 'vision')
|
||||||
op.drop_column('page_contents', 'mission')
|
op.drop_column('page_contents', 'mission')
|
||||||
op.drop_column('page_contents', 'about_hero_image')
|
op.drop_column('page_contents', 'about_hero_image')
|
||||||
|
|
||||||
@@ -1,26 +1,12 @@
|
|||||||
"""add_badges_to_page_content
|
|
||||||
|
|
||||||
Revision ID: add_badges_to_page_content
|
|
||||||
Revises: cce764ef7a50
|
|
||||||
Create Date: 2025-01-14 10:00:00.000000
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'add_badges_to_page_content'
|
revision = 'add_badges_to_page_content'
|
||||||
down_revision = 'cce764ef7a50'
|
down_revision = 'cce764ef7a50'
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
# Add badges column to page_contents table
|
|
||||||
op.add_column('page_contents', sa.Column('badges', sa.Text(), nullable=True))
|
op.add_column('page_contents', sa.Column('badges', sa.Text(), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
# Remove badges column from page_contents table
|
op.drop_column('page_contents', 'badges')
|
||||||
op.drop_column('page_contents', 'badges')
|
|
||||||
|
|
||||||
@@ -1,26 +1,12 @@
|
|||||||
"""add_copyright_text_to_page_content
|
|
||||||
|
|
||||||
Revision ID: a1b2c3d4e5f6
|
|
||||||
Revises: ff515d77abbe
|
|
||||||
Create Date: 2025-11-20 16:00:00.000000
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'a1b2c3d4e5f6'
|
revision = 'a1b2c3d4e5f6'
|
||||||
down_revision = '1444eb61188e'
|
down_revision = '1444eb61188e'
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
# Add copyright_text column to page_contents table
|
|
||||||
op.add_column('page_contents', sa.Column('copyright_text', sa.Text(), nullable=True))
|
op.add_column('page_contents', sa.Column('copyright_text', sa.Text(), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
# Remove copyright_text column from page_contents table
|
op.drop_column('page_contents', 'copyright_text')
|
||||||
op.drop_column('page_contents', 'copyright_text')
|
|
||||||
|
|
||||||
@@ -1,50 +1,19 @@
|
|||||||
"""add_stripe_payment_method
|
|
||||||
|
|
||||||
Revision ID: add_stripe_payment_method
|
|
||||||
Revises: 96c23dad405d
|
|
||||||
Create Date: 2025-01-17 12:00:00.000000
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy.dialects import mysql
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'add_stripe_payment_method'
|
revision = 'add_stripe_payment_method'
|
||||||
down_revision = '96c23dad405d'
|
down_revision = '96c23dad405d'
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
# Note: MySQL ENUM modifications can be tricky.
|
|
||||||
# If payments table already has data with existing enum values,
|
|
||||||
# we need to preserve them when adding 'stripe'
|
|
||||||
|
|
||||||
# For MySQL, we need to alter the ENUM column to include the new value
|
|
||||||
# Check if we're using MySQL
|
|
||||||
bind = op.get_bind()
|
bind = op.get_bind()
|
||||||
if bind.dialect.name == 'mysql':
|
if bind.dialect.name == 'mysql':
|
||||||
# Alter the ENUM column to include 'stripe'
|
op.execute("ALTER TABLE payments MODIFY COLUMN payment_method ENUM('cash', 'credit_card', 'debit_card', 'bank_transfer', 'e_wallet', 'stripe') NOT NULL")
|
||||||
# This preserves existing values and adds 'stripe'
|
|
||||||
op.execute(
|
|
||||||
"ALTER TABLE payments MODIFY COLUMN payment_method ENUM('cash', 'credit_card', 'debit_card', 'bank_transfer', 'e_wallet', 'stripe') NOT NULL"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
# For other databases (PostgreSQL, SQLite), enum changes are handled differently
|
|
||||||
# For SQLite, this might not be needed as it doesn't enforce enum constraints
|
|
||||||
pass
|
pass
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
# Remove 'stripe' from the ENUM (be careful if there are existing stripe payments)
|
|
||||||
bind = op.get_bind()
|
bind = op.get_bind()
|
||||||
if bind.dialect.name == 'mysql':
|
if bind.dialect.name == 'mysql':
|
||||||
# First, check if there are any stripe payments - if so, this will fail
|
op.execute("ALTER TABLE payments MODIFY COLUMN payment_method ENUM('cash', 'credit_card', 'debit_card', 'bank_transfer', 'e_wallet') NOT NULL")
|
||||||
# In production, you'd want to migrate existing stripe payments first
|
|
||||||
op.execute(
|
|
||||||
"ALTER TABLE payments MODIFY COLUMN payment_method ENUM('cash', 'credit_card', 'debit_card', 'bank_transfer', 'e_wallet') NOT NULL"
|
|
||||||
)
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
@@ -1,34 +1,18 @@
|
|||||||
"""add_promotion_fields_to_bookings
|
|
||||||
|
|
||||||
Revision ID: bd309b0742c1
|
|
||||||
Revises: f1a2b3c4d5e6
|
|
||||||
Create Date: 2025-11-20 02:16:09.496685
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'bd309b0742c1'
|
revision = 'bd309b0742c1'
|
||||||
down_revision = 'f1a2b3c4d5e6'
|
down_revision = 'f1a2b3c4d5e6'
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
# Add promotion-related columns to bookings table
|
|
||||||
op.add_column('bookings', sa.Column('original_price', sa.Numeric(10, 2), nullable=True))
|
op.add_column('bookings', sa.Column('original_price', sa.Numeric(10, 2), nullable=True))
|
||||||
op.add_column('bookings', sa.Column('discount_amount', sa.Numeric(10, 2), nullable=True, server_default='0'))
|
op.add_column('bookings', sa.Column('discount_amount', sa.Numeric(10, 2), nullable=True, server_default='0'))
|
||||||
op.add_column('bookings', sa.Column('promotion_code', sa.String(50), nullable=True))
|
op.add_column('bookings', sa.Column('promotion_code', sa.String(50), nullable=True))
|
||||||
# Add index on promotion_code for faster lookups
|
|
||||||
op.create_index(op.f('ix_bookings_promotion_code'), 'bookings', ['promotion_code'], unique=False)
|
op.create_index(op.f('ix_bookings_promotion_code'), 'bookings', ['promotion_code'], unique=False)
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
# Remove promotion-related columns
|
|
||||||
op.drop_index(op.f('ix_bookings_promotion_code'), table_name='bookings')
|
op.drop_index(op.f('ix_bookings_promotion_code'), table_name='bookings')
|
||||||
op.drop_column('bookings', 'promotion_code')
|
op.drop_column('bookings', 'promotion_code')
|
||||||
op.drop_column('bookings', 'discount_amount')
|
op.drop_column('bookings', 'discount_amount')
|
||||||
op.drop_column('bookings', 'original_price')
|
op.drop_column('bookings', 'original_price')
|
||||||
|
|
||||||
@@ -1,23 +1,11 @@
|
|||||||
"""add_luxury_content_fields_to_page_content
|
|
||||||
|
|
||||||
Revision ID: bfa74be4b256
|
|
||||||
Revises: bd309b0742c1
|
|
||||||
Create Date: 2025-11-20 13:27:52.106013
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'bfa74be4b256'
|
revision = 'bfa74be4b256'
|
||||||
down_revision = 'bd309b0742c1'
|
down_revision = 'bd309b0742c1'
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
# Add luxury content fields to page_contents table
|
|
||||||
op.add_column('page_contents', sa.Column('amenities_section_title', sa.String(500), nullable=True))
|
op.add_column('page_contents', sa.Column('amenities_section_title', sa.String(500), nullable=True))
|
||||||
op.add_column('page_contents', sa.Column('amenities_section_subtitle', sa.String(1000), nullable=True))
|
op.add_column('page_contents', sa.Column('amenities_section_subtitle', sa.String(1000), nullable=True))
|
||||||
op.add_column('page_contents', sa.Column('amenities', sa.Text(), nullable=True))
|
op.add_column('page_contents', sa.Column('amenities', sa.Text(), nullable=True))
|
||||||
@@ -33,9 +21,7 @@ def upgrade() -> None:
|
|||||||
op.add_column('page_contents', sa.Column('about_preview_image', sa.String(1000), nullable=True))
|
op.add_column('page_contents', sa.Column('about_preview_image', sa.String(1000), nullable=True))
|
||||||
op.add_column('page_contents', sa.Column('stats', sa.Text(), nullable=True))
|
op.add_column('page_contents', sa.Column('stats', sa.Text(), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
# Remove luxury content fields
|
|
||||||
op.drop_column('page_contents', 'stats')
|
op.drop_column('page_contents', 'stats')
|
||||||
op.drop_column('page_contents', 'about_preview_image')
|
op.drop_column('page_contents', 'about_preview_image')
|
||||||
op.drop_column('page_contents', 'about_preview_content')
|
op.drop_column('page_contents', 'about_preview_content')
|
||||||
@@ -49,5 +35,4 @@ def downgrade() -> None:
|
|||||||
op.drop_column('page_contents', 'testimonials_section_title')
|
op.drop_column('page_contents', 'testimonials_section_title')
|
||||||
op.drop_column('page_contents', 'amenities')
|
op.drop_column('page_contents', 'amenities')
|
||||||
op.drop_column('page_contents', 'amenities_section_subtitle')
|
op.drop_column('page_contents', 'amenities_section_subtitle')
|
||||||
op.drop_column('page_contents', 'amenities_section_title')
|
op.drop_column('page_contents', 'amenities_section_title')
|
||||||
|
|
||||||
@@ -1,28 +1,12 @@
|
|||||||
"""add_map_url_to_page_content
|
|
||||||
|
|
||||||
Revision ID: cce764ef7a50
|
|
||||||
Revises: 163657e72e93
|
|
||||||
Create Date: 2025-11-18 18:11:41.071053
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'cce764ef7a50'
|
revision = 'cce764ef7a50'
|
||||||
down_revision = '163657e72e93'
|
down_revision = '163657e72e93'
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
# Only add the map_url column to page_contents table
|
|
||||||
op.add_column('page_contents', sa.Column('map_url', sa.String(length=1000), nullable=True))
|
op.add_column('page_contents', sa.Column('map_url', sa.String(length=1000), nullable=True))
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
op.drop_column('page_contents', 'map_url')
|
||||||
op.drop_column('page_contents', 'map_url')
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
@@ -1,51 +1,19 @@
|
|||||||
"""add_paypal_payment_method
|
|
||||||
|
|
||||||
Revision ID: d9aff6c5f0d4
|
|
||||||
Revises: 08e2f866e131
|
|
||||||
Create Date: 2025-11-19 12:07:50.703320
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy.dialects import mysql
|
from sqlalchemy.dialects import mysql
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'd9aff6c5f0d4'
|
revision = 'd9aff6c5f0d4'
|
||||||
down_revision = '08e2f866e131'
|
down_revision = '08e2f866e131'
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
# Note: MySQL ENUM modifications can be tricky.
|
|
||||||
# If payments table already has data with existing enum values,
|
|
||||||
# we need to preserve them when adding 'paypal'
|
|
||||||
|
|
||||||
# For MySQL, we need to alter the ENUM column to include the new value
|
|
||||||
# Check if we're using MySQL
|
|
||||||
bind = op.get_bind()
|
bind = op.get_bind()
|
||||||
if bind.dialect.name == 'mysql':
|
if bind.dialect.name == 'mysql':
|
||||||
# Alter the ENUM column to include 'paypal'
|
op.execute("ALTER TABLE payments MODIFY COLUMN payment_method ENUM('cash', 'credit_card', 'debit_card', 'bank_transfer', 'e_wallet', 'stripe', 'paypal') NOT NULL")
|
||||||
# This preserves existing values and adds 'paypal'
|
|
||||||
op.execute(
|
|
||||||
"ALTER TABLE payments MODIFY COLUMN payment_method ENUM('cash', 'credit_card', 'debit_card', 'bank_transfer', 'e_wallet', 'stripe', 'paypal') NOT NULL"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
# For other databases (PostgreSQL, SQLite), enum changes are handled differently
|
|
||||||
# For SQLite, this might not be needed as it doesn't enforce enum constraints
|
|
||||||
pass
|
pass
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
# Remove 'paypal' from the ENUM (be careful if there are existing paypal payments)
|
|
||||||
bind = op.get_bind()
|
bind = op.get_bind()
|
||||||
if bind.dialect.name == 'mysql':
|
if bind.dialect.name == 'mysql':
|
||||||
# First, check if there are any paypal payments - if so, this will fail
|
op.execute("ALTER TABLE payments MODIFY COLUMN payment_method ENUM('cash', 'credit_card', 'debit_card', 'bank_transfer', 'e_wallet', 'stripe') NOT NULL")
|
||||||
# In production, you'd want to migrate existing paypal payments first
|
|
||||||
op.execute(
|
|
||||||
"ALTER TABLE payments MODIFY COLUMN payment_method ENUM('cash', 'credit_card', 'debit_card', 'bank_transfer', 'e_wallet', 'stripe') NOT NULL"
|
|
||||||
)
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
@@ -1,27 +1,12 @@
|
|||||||
"""add_is_proforma_to_invoices
|
|
||||||
|
|
||||||
Revision ID: f1a2b3c4d5e6
|
|
||||||
Revises: d9aff6c5f0d4
|
|
||||||
Create Date: 2025-11-20 00:20:00.000000
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'f1a2b3c4d5e6'
|
revision = 'f1a2b3c4d5e6'
|
||||||
down_revision = 'd9aff6c5f0d4'
|
down_revision = 'd9aff6c5f0d4'
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
# Add is_proforma column to invoices table
|
|
||||||
op.add_column('invoices', sa.Column('is_proforma', sa.Boolean(), nullable=False, server_default='0'))
|
op.add_column('invoices', sa.Column('is_proforma', sa.Boolean(), nullable=False, server_default='0'))
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
# Remove is_proforma column
|
op.drop_column('invoices', 'is_proforma')
|
||||||
op.drop_column('invoices', 'is_proforma')
|
|
||||||
|
|
||||||
@@ -1,36 +1,22 @@
|
|||||||
"""add_more_luxury_sections_to_page_content
|
|
||||||
|
|
||||||
Revision ID: ff515d77abbe
|
|
||||||
Revises: 17efc6439cc3
|
|
||||||
Create Date: 2025-11-20 15:17:50.977961
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = 'ff515d77abbe'
|
revision = 'ff515d77abbe'
|
||||||
down_revision = '17efc6439cc3'
|
down_revision = '17efc6439cc3'
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
# Add more luxury sections to page_contents table
|
op.add_column('page_contents', sa.Column('luxury_services', sa.Text(), nullable=True))
|
||||||
op.add_column('page_contents', sa.Column('luxury_services', sa.Text(), nullable=True)) # JSON array of services
|
op.add_column('page_contents', sa.Column('luxury_experiences', sa.Text(), nullable=True))
|
||||||
op.add_column('page_contents', sa.Column('luxury_experiences', sa.Text(), nullable=True)) # JSON array of experiences
|
op.add_column('page_contents', sa.Column('awards', sa.Text(), nullable=True))
|
||||||
op.add_column('page_contents', sa.Column('awards', sa.Text(), nullable=True)) # JSON array of awards
|
|
||||||
op.add_column('page_contents', sa.Column('cta_title', sa.Text(), nullable=True))
|
op.add_column('page_contents', sa.Column('cta_title', sa.Text(), nullable=True))
|
||||||
op.add_column('page_contents', sa.Column('cta_subtitle', sa.Text(), nullable=True))
|
op.add_column('page_contents', sa.Column('cta_subtitle', sa.Text(), nullable=True))
|
||||||
op.add_column('page_contents', sa.Column('cta_button_text', sa.Text(), nullable=True))
|
op.add_column('page_contents', sa.Column('cta_button_text', sa.Text(), nullable=True))
|
||||||
op.add_column('page_contents', sa.Column('cta_button_link', sa.Text(), nullable=True))
|
op.add_column('page_contents', sa.Column('cta_button_link', sa.Text(), nullable=True))
|
||||||
op.add_column('page_contents', sa.Column('cta_image', sa.Text(), nullable=True))
|
op.add_column('page_contents', sa.Column('cta_image', sa.Text(), nullable=True))
|
||||||
op.add_column('page_contents', sa.Column('partners', sa.Text(), nullable=True)) # JSON array of partners
|
op.add_column('page_contents', sa.Column('partners', sa.Text(), nullable=True))
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
# Remove luxury sections fields
|
|
||||||
op.drop_column('page_contents', 'partners')
|
op.drop_column('page_contents', 'partners')
|
||||||
op.drop_column('page_contents', 'cta_image')
|
op.drop_column('page_contents', 'cta_image')
|
||||||
op.drop_column('page_contents', 'cta_button_link')
|
op.drop_column('page_contents', 'cta_button_link')
|
||||||
@@ -39,5 +25,4 @@ def downgrade() -> None:
|
|||||||
op.drop_column('page_contents', 'cta_title')
|
op.drop_column('page_contents', 'cta_title')
|
||||||
op.drop_column('page_contents', 'awards')
|
op.drop_column('page_contents', 'awards')
|
||||||
op.drop_column('page_contents', 'luxury_experiences')
|
op.drop_column('page_contents', 'luxury_experiences')
|
||||||
op.drop_column('page_contents', 'luxury_services')
|
op.drop_column('page_contents', 'luxury_services')
|
||||||
|
|
||||||
@@ -1,13 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
|
||||||
Script to reset passwords for test users
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import bcrypt
|
import bcrypt
|
||||||
|
|
||||||
# Add the src directory to the path
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
@@ -19,7 +15,6 @@ logger = setup_logging()
|
|||||||
|
|
||||||
|
|
||||||
def hash_password(password: str) -> str:
|
def hash_password(password: str) -> str:
|
||||||
"""Hash password using bcrypt"""
|
|
||||||
password_bytes = password.encode('utf-8')
|
password_bytes = password.encode('utf-8')
|
||||||
salt = bcrypt.gensalt()
|
salt = bcrypt.gensalt()
|
||||||
hashed = bcrypt.hashpw(password_bytes, salt)
|
hashed = bcrypt.hashpw(password_bytes, salt)
|
||||||
@@ -27,17 +22,14 @@ def hash_password(password: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def reset_password(db: Session, email: str, new_password: str) -> bool:
|
def reset_password(db: Session, email: str, new_password: str) -> bool:
|
||||||
"""Reset password for a user"""
|
|
||||||
user = db.query(User).filter(User.email == email).first()
|
user = db.query(User).filter(User.email == email).first()
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
print(f"❌ User with email '{email}' not found")
|
print(f"❌ User with email '{email}' not found")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Hash new password
|
|
||||||
hashed_password = hash_password(new_password)
|
hashed_password = hash_password(new_password)
|
||||||
|
|
||||||
# Update password
|
|
||||||
user.password = hashed_password
|
user.password = hashed_password
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(user)
|
db.refresh(user)
|
||||||
@@ -51,7 +43,6 @@ def reset_password(db: Session, email: str, new_password: str) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Reset passwords for all test users"""
|
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -60,23 +51,6 @@ def main():
|
|||||||
print("="*80)
|
print("="*80)
|
||||||
print()
|
print()
|
||||||
|
|
||||||
test_users = [
|
|
||||||
{"email": "admin@hotel.com", "password": "admin123"},
|
|
||||||
{"email": "staff@hotel.com", "password": "staff123"},
|
|
||||||
{"email": "customer@hotel.com", "password": "customer123"},
|
|
||||||
]
|
|
||||||
|
|
||||||
for user_data in test_users:
|
|
||||||
reset_password(db, user_data["email"], user_data["password"])
|
|
||||||
|
|
||||||
print("="*80)
|
|
||||||
print("SUMMARY")
|
|
||||||
print("="*80)
|
|
||||||
print("All test user passwords have been reset.")
|
|
||||||
print("\nYou can now login with:")
|
|
||||||
for user_data in test_users:
|
|
||||||
print(f" {user_data['email']:<25} Password: {user_data['password']}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error: {e}", exc_info=True)
|
logger.error(f"Error: {e}", exc_info=True)
|
||||||
@@ -88,4 +62,3 @@ def main():
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
||||||
|
|||||||
@@ -1,48 +1,13 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Main entry point for the FastAPI server
|
|
||||||
"""
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from src.config.settings import settings
|
from src.config.settings import settings
|
||||||
from src.config.logging_config import setup_logging, get_logger
|
from src.config.logging_config import setup_logging, get_logger
|
||||||
|
|
||||||
# Setup logging
|
|
||||||
setup_logging()
|
setup_logging()
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
if __name__ == '__main__':
|
||||||
if __name__ == "__main__":
|
logger.info(f'Starting {settings.APP_NAME} on {settings.HOST}:{settings.PORT}')
|
||||||
logger.info(f"Starting {settings.APP_NAME} on {settings.HOST}:{settings.PORT}")
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Only watch the src directory to avoid watching logs, uploads, etc.
|
|
||||||
base_dir = Path(__file__).parent
|
base_dir = Path(__file__).parent
|
||||||
src_dir = str(base_dir / "src")
|
src_dir = str(base_dir / 'src')
|
||||||
|
use_reload = False
|
||||||
# Temporarily disable reload to stop constant "1 change detected" messages
|
uvicorn.run('src.main:app', host=settings.HOST, port=8000, reload=use_reload, log_level=settings.LOG_LEVEL.lower(), reload_dirs=[src_dir] if use_reload else None, reload_excludes=['*.log', '*.pyc', '*.pyo', '*.pyd', '__pycache__', '**/__pycache__/**', '*.db', '*.sqlite', '*.sqlite3'], reload_delay=1.0)
|
||||||
# The file watcher is detecting changes that cause a loop
|
|
||||||
# TODO: Investigate what's causing constant file changes
|
|
||||||
use_reload = False # Disabled until we identify the source of constant changes
|
|
||||||
|
|
||||||
uvicorn.run(
|
|
||||||
"src.main:app",
|
|
||||||
host=settings.HOST,
|
|
||||||
port=8000,
|
|
||||||
reload=use_reload,
|
|
||||||
log_level=settings.LOG_LEVEL.lower(),
|
|
||||||
reload_dirs=[src_dir] if use_reload else None,
|
|
||||||
reload_excludes=[
|
|
||||||
"*.log",
|
|
||||||
"*.pyc",
|
|
||||||
"*.pyo",
|
|
||||||
"*.pyd",
|
|
||||||
"__pycache__",
|
|
||||||
"**/__pycache__/**",
|
|
||||||
"*.db",
|
|
||||||
"*.sqlite",
|
|
||||||
"*.sqlite3"
|
|
||||||
],
|
|
||||||
reload_delay=1.0 # Increase delay to reduce false positives
|
|
||||||
)
|
|
||||||
|
|
||||||
@@ -1,14 +1,10 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
|
||||||
Seed sample data for the About page
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import json
|
import json
|
||||||
|
|
||||||
# Add the parent directory to the path so we can import from src
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent))
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
@@ -17,7 +13,6 @@ from src.models.page_content import PageContent, PageType
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
def get_db():
|
def get_db():
|
||||||
"""Get database session"""
|
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
return db
|
return db
|
||||||
@@ -25,23 +20,16 @@ def get_db():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def seed_about_page(db: Session):
|
def seed_about_page(db: Session):
|
||||||
"""Seed about page content"""
|
|
||||||
print("=" * 80)
|
print("=" * 80)
|
||||||
print("SEEDING ABOUT PAGE CONTENT")
|
print("SEEDING ABOUT PAGE CONTENT")
|
||||||
print("=" * 80)
|
print("=" * 80)
|
||||||
|
|
||||||
# Sample data
|
|
||||||
about_data = {
|
about_data = {
|
||||||
"title": "About Luxury Hotel",
|
"title": "About Luxury Hotel",
|
||||||
"subtitle": "Where Excellence Meets Unforgettable Experiences",
|
"subtitle": "Where Excellence Meets Unforgettable Experiences",
|
||||||
"description": "Discover the story behind our commitment to luxury hospitality and exceptional service.",
|
"description": "Discover the story behind our commitment to luxury hospitality and exceptional service.",
|
||||||
"story_content": """Welcome to Luxury Hotel, where timeless elegance meets modern sophistication. Since our founding in 2010, we have been dedicated to providing exceptional hospitality and creating unforgettable memories for our guests.
|
"story_content":
|
||||||
|
,
|
||||||
Nestled in the heart of the city, our hotel combines classic architecture with contemporary amenities, offering a perfect blend of comfort and luxury. Every detail has been carefully curated to ensure your stay exceeds expectations.
|
|
||||||
|
|
||||||
Our commitment to excellence extends beyond our beautiful rooms and facilities. We believe in creating meaningful connections with our guests, understanding their needs, and delivering personalized service that makes each visit special.
|
|
||||||
|
|
||||||
Over the years, we have hosted thousands of guests from around the world, each leaving with cherished memories and a desire to return. Our team of dedicated professionals works tirelessly to ensure that every moment of your stay is perfect.""",
|
|
||||||
"mission": "To provide unparalleled luxury hospitality experiences that exceed expectations, creating lasting memories for our guests through exceptional service, attention to detail, and genuine care.",
|
"mission": "To provide unparalleled luxury hospitality experiences that exceed expectations, creating lasting memories for our guests through exceptional service, attention to detail, and genuine care.",
|
||||||
"vision": "To be recognized as the world's premier luxury hotel brand, setting the standard for excellence in hospitality while maintaining our commitment to sustainability and community engagement.",
|
"vision": "To be recognized as the world's premier luxury hotel brand, setting the standard for excellence in hospitality while maintaining our commitment to sustainability and community engagement.",
|
||||||
"about_hero_image": "https://images.unsplash.com/photo-1566073771259-6a8506099945?w=1920&h=1080&fit=crop",
|
"about_hero_image": "https://images.unsplash.com/photo-1566073771259-6a8506099945?w=1920&h=1080&fit=crop",
|
||||||
@@ -181,17 +169,14 @@ Over the years, we have hosted thousands of guests from around the world, each l
|
|||||||
"meta_description": "Learn about Luxury Hotel's commitment to excellence, our story, values, and the dedicated team that makes every stay unforgettable."
|
"meta_description": "Learn about Luxury Hotel's commitment to excellence, our story, values, and the dedicated team that makes every stay unforgettable."
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check if about page content exists
|
|
||||||
existing = db.query(PageContent).filter(PageContent.page_type == PageType.ABOUT).first()
|
existing = db.query(PageContent).filter(PageContent.page_type == PageType.ABOUT).first()
|
||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
# Update existing
|
|
||||||
for key, value in about_data.items():
|
for key, value in about_data.items():
|
||||||
setattr(existing, key, value)
|
setattr(existing, key, value)
|
||||||
existing.updated_at = datetime.utcnow()
|
existing.updated_at = datetime.utcnow()
|
||||||
print("✓ Updated existing about page content")
|
print("✓ Updated existing about page content")
|
||||||
else:
|
else:
|
||||||
# Create new
|
|
||||||
new_content = PageContent(
|
new_content = PageContent(
|
||||||
page_type=PageType.ABOUT,
|
page_type=PageType.ABOUT,
|
||||||
**about_data
|
**about_data
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
"""
|
|
||||||
Seed script to populate banners and company information with sample data.
|
|
||||||
Run this script to add default banners and company settings.
|
|
||||||
"""
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
# Add the src directory to the path
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from src.config.database import SessionLocal
|
from src.config.database import SessionLocal
|
||||||
from src.models.banner import Banner
|
from src.models.banner import Banner
|
||||||
@@ -16,199 +9,62 @@ from src.models.system_settings import SystemSettings
|
|||||||
from src.models.user import User
|
from src.models.user import User
|
||||||
|
|
||||||
def seed_banners(db: Session):
|
def seed_banners(db: Session):
|
||||||
"""Seed sample banners"""
|
print('Seeding banners...')
|
||||||
print("Seeding banners...")
|
admin_user = db.query(User).filter(User.email == 'admin@hotel.com').first()
|
||||||
|
|
||||||
# Get admin user for updated_by_id (if exists)
|
|
||||||
admin_user = db.query(User).filter(User.email == "admin@hotel.com").first()
|
|
||||||
admin_id = admin_user.id if admin_user else None
|
admin_id = admin_user.id if admin_user else None
|
||||||
|
|
||||||
# Delete all existing banners
|
|
||||||
existing_banners = db.query(Banner).all()
|
existing_banners = db.query(Banner).all()
|
||||||
if existing_banners:
|
if existing_banners:
|
||||||
for banner in existing_banners:
|
for banner in existing_banners:
|
||||||
db.delete(banner)
|
db.delete(banner)
|
||||||
db.commit()
|
db.commit()
|
||||||
print(f" ✓ Removed {len(existing_banners)} existing banner(s)")
|
print(f' ✓ Removed {len(existing_banners)} existing banner(s)')
|
||||||
|
banners_data = [{'title': 'Welcome to Unparalleled Luxury', 'description': 'Where timeless elegance meets modern sophistication. Experience the pinnacle of hospitality in our award-winning luxury hotel.', 'image_url': 'https://images.unsplash.com/photo-1564501049412-61c2a3083791?w=1920', 'link_url': '/rooms', 'position': 'home', 'display_order': 1, 'is_active': True, 'start_date': datetime.utcnow() - timedelta(days=30), 'end_date': datetime.utcnow() + timedelta(days=365)}, {'title': 'Exclusive Presidential Suites', 'description': 'Indulge in our most opulent accommodations. Spacious suites with panoramic views, private terraces, and personalized butler service.', 'image_url': 'https://images.unsplash.com/photo-1590490360182-c33d57733427?w=1920', 'link_url': '/rooms', 'position': 'home', 'display_order': 2, 'is_active': True, 'start_date': datetime.utcnow() - timedelta(days=7), 'end_date': datetime.utcnow() + timedelta(days=365)}, {'title': 'World-Class Spa & Wellness', 'description': 'Rejuvenate your mind, body, and soul. Our award-winning spa offers bespoke treatments using the finest luxury products.', 'image_url': 'https://images.unsplash.com/photo-1544161515-4ab6ce6db874?w=1920', 'link_url': '/services', 'position': 'home', 'display_order': 3, 'is_active': True, 'start_date': datetime.utcnow() - timedelta(days=1), 'end_date': datetime.utcnow() + timedelta(days=365)}, {'title': 'Michelin-Starred Culinary Excellence', 'description': 'Savor extraordinary flavors crafted by world-renowned chefs. Our fine dining restaurants offer an unforgettable gastronomic journey.', 'image_url': 'https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=1920', 'link_url': '/services', 'position': 'home', 'display_order': 4, 'is_active': True, 'start_date': datetime.utcnow(), 'end_date': datetime.utcnow() + timedelta(days=365)}, {'title': 'Private Yacht & Exclusive Experiences', 'description': 'Create unforgettable memories with our curated luxury experiences. From private yacht charters to exclusive cultural tours.', 'image_url': 'https://images.unsplash.com/photo-1544551763-46a013bb70d5?w=1920', 'link_url': '/services', 'position': 'home', 'display_order': 5, 'is_active': True, 'start_date': datetime.utcnow() - timedelta(days=15), 'end_date': datetime.utcnow() + timedelta(days=365)}]
|
||||||
# New luxury banners with premium content
|
|
||||||
banners_data = [
|
|
||||||
{
|
|
||||||
"title": "Welcome to Unparalleled Luxury",
|
|
||||||
"description": "Where timeless elegance meets modern sophistication. Experience the pinnacle of hospitality in our award-winning luxury hotel.",
|
|
||||||
"image_url": "https://images.unsplash.com/photo-1564501049412-61c2a3083791?w=1920",
|
|
||||||
"link_url": "/rooms",
|
|
||||||
"position": "home",
|
|
||||||
"display_order": 1,
|
|
||||||
"is_active": True,
|
|
||||||
"start_date": datetime.utcnow() - timedelta(days=30),
|
|
||||||
"end_date": datetime.utcnow() + timedelta(days=365),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Exclusive Presidential Suites",
|
|
||||||
"description": "Indulge in our most opulent accommodations. Spacious suites with panoramic views, private terraces, and personalized butler service.",
|
|
||||||
"image_url": "https://images.unsplash.com/photo-1590490360182-c33d57733427?w=1920",
|
|
||||||
"link_url": "/rooms",
|
|
||||||
"position": "home",
|
|
||||||
"display_order": 2,
|
|
||||||
"is_active": True,
|
|
||||||
"start_date": datetime.utcnow() - timedelta(days=7),
|
|
||||||
"end_date": datetime.utcnow() + timedelta(days=365),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "World-Class Spa & Wellness",
|
|
||||||
"description": "Rejuvenate your mind, body, and soul. Our award-winning spa offers bespoke treatments using the finest luxury products.",
|
|
||||||
"image_url": "https://images.unsplash.com/photo-1544161515-4ab6ce6db874?w=1920",
|
|
||||||
"link_url": "/services",
|
|
||||||
"position": "home",
|
|
||||||
"display_order": 3,
|
|
||||||
"is_active": True,
|
|
||||||
"start_date": datetime.utcnow() - timedelta(days=1),
|
|
||||||
"end_date": datetime.utcnow() + timedelta(days=365),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Michelin-Starred Culinary Excellence",
|
|
||||||
"description": "Savor extraordinary flavors crafted by world-renowned chefs. Our fine dining restaurants offer an unforgettable gastronomic journey.",
|
|
||||||
"image_url": "https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=1920",
|
|
||||||
"link_url": "/services",
|
|
||||||
"position": "home",
|
|
||||||
"display_order": 4,
|
|
||||||
"is_active": True,
|
|
||||||
"start_date": datetime.utcnow(),
|
|
||||||
"end_date": datetime.utcnow() + timedelta(days=365),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Private Yacht & Exclusive Experiences",
|
|
||||||
"description": "Create unforgettable memories with our curated luxury experiences. From private yacht charters to exclusive cultural tours.",
|
|
||||||
"image_url": "https://images.unsplash.com/photo-1544551763-46a013bb70d5?w=1920",
|
|
||||||
"link_url": "/services",
|
|
||||||
"position": "home",
|
|
||||||
"display_order": 5,
|
|
||||||
"is_active": True,
|
|
||||||
"start_date": datetime.utcnow() - timedelta(days=15),
|
|
||||||
"end_date": datetime.utcnow() + timedelta(days=365),
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
for banner_data in banners_data:
|
for banner_data in banners_data:
|
||||||
# Create new banner
|
|
||||||
new_banner = Banner(**banner_data)
|
new_banner = Banner(**banner_data)
|
||||||
db.add(new_banner)
|
db.add(new_banner)
|
||||||
print(f" ✓ Created banner: {banner_data['title']}")
|
print(f' ✓ Created banner: {banner_data['title']}')
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
print("✓ Banners seeded successfully!\n")
|
print('✓ Banners seeded successfully!\n')
|
||||||
|
|
||||||
def seed_company_info(db: Session):
|
def seed_company_info(db: Session):
|
||||||
"""Seed company information"""
|
print('Seeding company information...')
|
||||||
print("Seeding company information...")
|
admin_user = db.query(User).filter(User.email == 'admin@hotel.com').first()
|
||||||
|
|
||||||
# Get admin user for updated_by_id (if exists)
|
|
||||||
admin_user = db.query(User).filter(User.email == "admin@hotel.com").first()
|
|
||||||
admin_id = admin_user.id if admin_user else None
|
admin_id = admin_user.id if admin_user else None
|
||||||
|
company_settings = [{'key': 'company_name', 'value': 'Luxury Hotel', 'description': 'Company name displayed throughout the application'}, {'key': 'company_tagline', 'value': 'Experience Unparalleled Elegance', 'description': 'Company tagline or slogan'}, {'key': 'company_logo_url', 'value': '', 'description': 'URL to company logo image (upload via admin dashboard)'}, {'key': 'company_favicon_url', 'value': '', 'description': 'URL to company favicon image (upload via admin dashboard)'}, {'key': 'company_phone', 'value': '+1 (555) 123-4567', 'description': 'Company contact phone number'}, {'key': 'company_email', 'value': 'info@luxuryhotel.com', 'description': 'Company contact email address'}, {'key': 'company_address', 'value': '123 Luxury Avenue, Premium District, City 12345, Country', 'description': 'Company physical address'}, {'key': 'tax_rate', 'value': '10.0', 'description': 'Default tax rate percentage (e.g., 10.0 for 10%)'}, {'key': 'platform_currency', 'value': 'EUR', 'description': 'Platform-wide currency setting for displaying prices'}]
|
||||||
# Company settings
|
|
||||||
company_settings = [
|
|
||||||
{
|
|
||||||
"key": "company_name",
|
|
||||||
"value": "Luxury Hotel",
|
|
||||||
"description": "Company name displayed throughout the application"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "company_tagline",
|
|
||||||
"value": "Experience Unparalleled Elegance",
|
|
||||||
"description": "Company tagline or slogan"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "company_logo_url",
|
|
||||||
"value": "",
|
|
||||||
"description": "URL to company logo image (upload via admin dashboard)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "company_favicon_url",
|
|
||||||
"value": "",
|
|
||||||
"description": "URL to company favicon image (upload via admin dashboard)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "company_phone",
|
|
||||||
"value": "+1 (555) 123-4567",
|
|
||||||
"description": "Company contact phone number"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "company_email",
|
|
||||||
"value": "info@luxuryhotel.com",
|
|
||||||
"description": "Company contact email address"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "company_address",
|
|
||||||
"value": "123 Luxury Avenue, Premium District, City 12345, Country",
|
|
||||||
"description": "Company physical address"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "tax_rate",
|
|
||||||
"value": "10.0",
|
|
||||||
"description": "Default tax rate percentage (e.g., 10.0 for 10%)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "platform_currency",
|
|
||||||
"value": "EUR",
|
|
||||||
"description": "Platform-wide currency setting for displaying prices"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
for setting_data in company_settings:
|
for setting_data in company_settings:
|
||||||
# Check if setting exists
|
existing = db.query(SystemSettings).filter(SystemSettings.key == setting_data['key']).first()
|
||||||
existing = db.query(SystemSettings).filter(
|
|
||||||
SystemSettings.key == setting_data["key"]
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
# Update existing setting
|
existing.value = setting_data['value']
|
||||||
existing.value = setting_data["value"]
|
existing.description = setting_data['description']
|
||||||
existing.description = setting_data["description"]
|
|
||||||
if admin_id:
|
if admin_id:
|
||||||
existing.updated_by_id = admin_id
|
existing.updated_by_id = admin_id
|
||||||
print(f" ✓ Updated setting: {setting_data['key']}")
|
print(f' ✓ Updated setting: {setting_data['key']}')
|
||||||
else:
|
else:
|
||||||
# Create new setting
|
new_setting = SystemSettings(key=setting_data['key'], value=setting_data['value'], description=setting_data['description'], updated_by_id=admin_id)
|
||||||
new_setting = SystemSettings(
|
|
||||||
key=setting_data["key"],
|
|
||||||
value=setting_data["value"],
|
|
||||||
description=setting_data["description"],
|
|
||||||
updated_by_id=admin_id
|
|
||||||
)
|
|
||||||
db.add(new_setting)
|
db.add(new_setting)
|
||||||
print(f" ✓ Created setting: {setting_data['key']}")
|
print(f' ✓ Created setting: {setting_data['key']}')
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
print("✓ Company information seeded successfully!\n")
|
print('✓ Company information seeded successfully!\n')
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Main seed function"""
|
|
||||||
db: Session = SessionLocal()
|
db: Session = SessionLocal()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
print("=" * 80)
|
print('=' * 80)
|
||||||
print("SEEDING BANNERS AND COMPANY INFORMATION")
|
print('SEEDING BANNERS AND COMPANY INFORMATION')
|
||||||
print("=" * 80)
|
print('=' * 80)
|
||||||
print()
|
print()
|
||||||
|
|
||||||
seed_banners(db)
|
seed_banners(db)
|
||||||
seed_company_info(db)
|
seed_company_info(db)
|
||||||
|
print('=' * 80)
|
||||||
print("=" * 80)
|
print('✓ All data seeded successfully!')
|
||||||
print("✓ All data seeded successfully!")
|
print('=' * 80)
|
||||||
print("=" * 80)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
print(f"\n✗ Error seeding data: {e}")
|
print(f'\n✗ Error seeding data: {e}')
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
if __name__ == '__main__':
|
||||||
if __name__ == "__main__":
|
main()
|
||||||
main()
|
|
||||||
|
|
||||||
@@ -1,456 +1,75 @@
|
|||||||
"""
|
|
||||||
Comprehensive seed script to populate homepage and footer with sample luxury content.
|
|
||||||
Run this script to add default content to the page_content table.
|
|
||||||
"""
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
|
||||||
# Add the src directory to the path
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from src.config.database import SessionLocal
|
from src.config.database import SessionLocal
|
||||||
from src.models.page_content import PageContent, PageType
|
from src.models.page_content import PageContent, PageType
|
||||||
|
|
||||||
def seed_homepage_content(db: Session):
|
def seed_homepage_content(db: Session):
|
||||||
"""Seed comprehensive homepage content"""
|
|
||||||
existing = db.query(PageContent).filter(PageContent.page_type == PageType.HOME).first()
|
existing = db.query(PageContent).filter(PageContent.page_type == PageType.HOME).first()
|
||||||
|
luxury_features = [{'icon': 'Sparkles', 'title': 'Premium Amenities', 'description': 'World-class facilities designed for your comfort and relaxation'}, {'icon': 'Crown', 'title': 'Royal Service', 'description': 'Dedicated concierge service available 24/7 for all your needs'}, {'icon': 'Award', 'title': 'Award-Winning', 'description': 'Recognized for excellence in hospitality and guest satisfaction'}, {'icon': 'Shield', 'title': 'Secure & Private', 'description': 'Your privacy and security are our top priorities'}, {'icon': 'Heart', 'title': 'Personalized Care', 'description': 'Tailored experiences crafted just for you'}, {'icon': 'Gem', 'title': 'Luxury Design', 'description': 'Elegantly designed spaces with attention to every detail'}]
|
||||||
# Luxury Features
|
luxury_gallery = ['https://images.unsplash.com/photo-1566073771259-6a8506099945?w=800', 'https://images.unsplash.com/photo-1571896349842-33c89424de2d?w=800', 'https://images.unsplash.com/photo-1582719478250-c89cae4dc85b?w=800', 'https://images.unsplash.com/photo-1564501049412-61c2a3083791?w=800', 'https://images.unsplash.com/photo-1590490360182-c33d57733427?w=800', 'https://images.unsplash.com/photo-1551882547-ff40c63fe5fa?w=800']
|
||||||
luxury_features = [
|
luxury_testimonials = [{'name': 'Sarah Johnson', 'title': 'Business Executive', 'quote': 'An absolutely stunning experience. The attention to detail and level of service exceeded all expectations.', 'image': 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=200'}, {'name': 'Michael Chen', 'title': 'Travel Enthusiast', 'quote': 'The epitome of luxury. Every moment was perfect, from check-in to check-out.', 'image': 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=200'}, {'name': 'Emma Williams', 'title': 'Luxury Traveler', 'quote': 'This hotel redefines what luxury means. I will definitely return.', 'image': 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=200'}]
|
||||||
{
|
luxury_services = [{'icon': 'UtensilsCrossed', 'title': 'Fine Dining', 'description': 'Michelin-starred restaurants offering world-class cuisine', 'image': 'https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=600'}, {'icon': 'Wine', 'title': 'Premium Bar', 'description': 'Extensive wine collection and craft cocktails in elegant settings', 'image': 'https://images.unsplash.com/photo-1514362545857-3bc16c4c7d1b?w=600'}, {'icon': 'Dumbbell', 'title': 'Spa & Wellness', 'description': 'Rejuvenating spa treatments and state-of-the-art fitness center', 'image': 'https://images.unsplash.com/photo-1544161515-4ab6ce6db874?w=600'}, {'icon': 'Car', 'title': 'Concierge Services', 'description': 'Personalized assistance for all your travel and entertainment needs', 'image': 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=600'}]
|
||||||
"icon": "Sparkles",
|
luxury_experiences = [{'icon': 'Sunset', 'title': 'Sunset Rooftop', 'description': 'Breathtaking views and exclusive rooftop experiences', 'image': 'https://images.unsplash.com/photo-1514933651103-005eec06c04b?w=600'}, {'icon': 'Ship', 'title': 'Yacht Excursions', 'description': 'Private yacht charters for unforgettable sea adventures', 'image': 'https://images.unsplash.com/photo-1544551763-46a013bb70d5?w=600'}, {'icon': 'Music', 'title': 'Live Entertainment', 'description': 'World-class performances and exclusive events', 'image': 'https://images.unsplash.com/photo-1470229722913-7c0e2dbbafd3?w=600'}, {'icon': 'Palette', 'title': 'Art & Culture', 'description': 'Curated art collections and cultural experiences', 'image': 'https://images.unsplash.com/photo-1578301978018-3005759f48f7?w=600'}]
|
||||||
"title": "Premium Amenities",
|
awards = [{'icon': 'Trophy', 'title': 'Best Luxury Hotel 2024', 'description': 'Awarded by International Luxury Travel Association', 'image': 'https://images.unsplash.com/photo-1579783902614-a3fb3927b6a5?w=400', 'year': '2024'}, {'icon': 'Star', 'title': '5-Star Excellence', 'description': 'Consistently rated 5 stars by leading travel publications', 'image': 'https://images.unsplash.com/photo-1606761568499-6d2451b23c66?w=400', 'year': '2023'}, {'icon': 'Award', 'title': 'Sustainable Luxury', 'description': 'Recognized for environmental responsibility and sustainability', 'image': 'https://images.unsplash.com/photo-1473341304170-971dccb5ac1e?w=400', 'year': '2024'}]
|
||||||
"description": "World-class facilities designed for your comfort and relaxation"
|
partners = [{'name': 'Luxury Travel Group', 'logo': 'https://images.unsplash.com/photo-1599305445671-ac291c95aaa9?w=200', 'link': '#'}, {'name': 'Premium Airlines', 'logo': 'https://images.unsplash.com/photo-1436491865332-7a61a109cc05?w=200', 'link': '#'}, {'name': 'Exclusive Events', 'logo': 'https://images.unsplash.com/photo-1511578314322-379afb476865?w=200', 'link': '#'}, {'name': 'Fine Dining Network', 'logo': 'https://images.unsplash.com/photo-1555396273-367ea4eb4db5?w=200', 'link': '#'}]
|
||||||
},
|
stats = [{'icon': 'Users', 'number': '50,000+', 'label': 'Happy Guests'}, {'icon': 'Award', 'number': '25+', 'label': 'Awards Won'}, {'icon': 'Star', 'number': '4.9', 'label': 'Average Rating'}, {'icon': 'Globe', 'number': '100+', 'label': 'Countries Served'}]
|
||||||
{
|
amenities = [{'icon': 'Wifi', 'title': 'High-Speed WiFi', 'description': 'Complimentary high-speed internet throughout the property', 'image': ''}, {'icon': 'Coffee', 'title': '24/7 Room Service', 'description': 'Round-the-clock dining and beverage service', 'image': ''}, {'icon': 'Car', 'title': 'Valet Parking', 'description': 'Complimentary valet parking for all guests', 'image': ''}, {'icon': 'Plane', 'title': 'Airport Transfer', 'description': 'Luxury airport transfer service available', 'image': ''}]
|
||||||
"icon": "Crown",
|
testimonials = [{'name': 'Robert Martinez', 'role': 'CEO, Tech Corp', 'image': 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=200', 'rating': 5, 'comment': 'Exceptional service and attention to detail. The staff went above and beyond to make our stay memorable.'}, {'name': 'Lisa Anderson', 'role': 'Travel Blogger', 'image': 'https://images.unsplash.com/photo-1487412720507-e7ab37603c6f?w=200', 'rating': 5, 'comment': "The most luxurious hotel experience I've ever had. Every detail was perfect."}, {'name': 'David Thompson', 'role': 'Investment Banker', 'image': 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=200', 'rating': 5, 'comment': 'Outstanding facilities and impeccable service. Highly recommend for business travelers.'}]
|
||||||
"title": "Royal Service",
|
gallery_images = ['https://images.unsplash.com/photo-1564501049412-61c2a3083791?w=800', 'https://images.unsplash.com/photo-1590490360182-c33d57733427?w=800', 'https://images.unsplash.com/photo-1551882547-ff40c63fe5fa?w=800', 'https://images.unsplash.com/photo-1571896349842-33c89424de2d?w=800', 'https://images.unsplash.com/photo-1582719478250-c89cae4dc85b?w=800', 'https://images.unsplash.com/photo-1566073771259-6a8506099945?w=800']
|
||||||
"description": "Dedicated concierge service available 24/7 for all your needs"
|
homepage_data = {'page_type': PageType.HOME, 'title': 'Luxury Hotel - Experience Unparalleled Elegance', 'subtitle': 'Where timeless luxury meets modern sophistication', 'description': 'Discover a world of refined elegance and exceptional service', 'hero_title': 'Welcome to Luxury', 'hero_subtitle': 'Experience the pinnacle of hospitality', 'hero_image': 'https://images.unsplash.com/photo-1564501049412-61c2a3083791?w=1200', 'luxury_section_title': 'Experience Unparalleled Luxury', 'luxury_section_subtitle': 'Where elegance meets comfort in every detail', 'luxury_section_image': 'https://images.unsplash.com/photo-1571896349842-33c89424de2d?w=1200', 'luxury_features': json.dumps(luxury_features), 'luxury_gallery_section_title': 'Our Luxury Gallery', 'luxury_gallery_section_subtitle': 'A glimpse into our world of elegance', 'luxury_gallery': json.dumps(luxury_gallery), 'luxury_testimonials_section_title': 'What Our Guests Say', 'luxury_testimonials_section_subtitle': 'Testimonials from our valued guests', 'luxury_testimonials': json.dumps(luxury_testimonials), 'luxury_services_section_title': 'Premium Services', 'luxury_services_section_subtitle': 'Indulge in our world-class amenities', 'luxury_services': json.dumps(luxury_services), 'luxury_experiences_section_title': 'Exclusive Experiences', 'luxury_experiences_section_subtitle': 'Create unforgettable memories', 'luxury_experiences': json.dumps(luxury_experiences), 'awards_section_title': 'Awards & Recognition', 'awards_section_subtitle': 'Recognized for excellence worldwide', 'awards': json.dumps(awards), 'partners_section_title': 'Our Partners', 'partners_section_subtitle': 'Trusted by leading brands', 'partners': json.dumps(partners), 'amenities_section_title': 'Premium Amenities', 'amenities_section_subtitle': 'Everything you need for a perfect stay', 'amenities': json.dumps(amenities), 'testimonials_section_title': 'Guest Reviews', 'testimonials_section_subtitle': 'Hear from our satisfied guests', 'testimonials': json.dumps(testimonials), 'gallery_section_title': 'Photo Gallery', 'gallery_section_subtitle': 'Explore our beautiful spaces', 'gallery_images': json.dumps(gallery_images), 'about_preview_title': 'About Our Luxury Hotel', 'about_preview_subtitle': 'A legacy of excellence', 'about_preview_content': 'Discover a world of refined elegance and exceptional service. Our hotel combines timeless luxury with modern amenities to create an unforgettable experience. With over 50,000 satisfied guests and numerous awards, we continue to set the standard for luxury hospitality.', 'about_preview_image': 'https://images.unsplash.com/photo-1582719478250-c89cae4dc85b?w=800', 'stats': json.dumps(stats), 'cta_title': 'Ready to Experience Luxury?', 'cta_subtitle': 'Book your stay today and discover the difference', 'cta_button_text': 'Book Now', 'cta_button_link': '/rooms', 'cta_image': 'https://images.unsplash.com/photo-1566073771259-6a8506099945?w=1200', 'is_active': True}
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon": "Award",
|
|
||||||
"title": "Award-Winning",
|
|
||||||
"description": "Recognized for excellence in hospitality and guest satisfaction"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon": "Shield",
|
|
||||||
"title": "Secure & Private",
|
|
||||||
"description": "Your privacy and security are our top priorities"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon": "Heart",
|
|
||||||
"title": "Personalized Care",
|
|
||||||
"description": "Tailored experiences crafted just for you"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon": "Gem",
|
|
||||||
"title": "Luxury Design",
|
|
||||||
"description": "Elegantly designed spaces with attention to every detail"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# Luxury Gallery
|
|
||||||
luxury_gallery = [
|
|
||||||
"https://images.unsplash.com/photo-1566073771259-6a8506099945?w=800",
|
|
||||||
"https://images.unsplash.com/photo-1571896349842-33c89424de2d?w=800",
|
|
||||||
"https://images.unsplash.com/photo-1582719478250-c89cae4dc85b?w=800",
|
|
||||||
"https://images.unsplash.com/photo-1564501049412-61c2a3083791?w=800",
|
|
||||||
"https://images.unsplash.com/photo-1590490360182-c33d57733427?w=800",
|
|
||||||
"https://images.unsplash.com/photo-1551882547-ff40c63fe5fa?w=800"
|
|
||||||
]
|
|
||||||
|
|
||||||
# Luxury Testimonials
|
|
||||||
luxury_testimonials = [
|
|
||||||
{
|
|
||||||
"name": "Sarah Johnson",
|
|
||||||
"title": "Business Executive",
|
|
||||||
"quote": "An absolutely stunning experience. The attention to detail and level of service exceeded all expectations.",
|
|
||||||
"image": "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=200"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Michael Chen",
|
|
||||||
"title": "Travel Enthusiast",
|
|
||||||
"quote": "The epitome of luxury. Every moment was perfect, from check-in to check-out.",
|
|
||||||
"image": "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=200"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Emma Williams",
|
|
||||||
"title": "Luxury Traveler",
|
|
||||||
"quote": "This hotel redefines what luxury means. I will definitely return.",
|
|
||||||
"image": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=200"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# Luxury Services
|
|
||||||
luxury_services = [
|
|
||||||
{
|
|
||||||
"icon": "UtensilsCrossed",
|
|
||||||
"title": "Fine Dining",
|
|
||||||
"description": "Michelin-starred restaurants offering world-class cuisine",
|
|
||||||
"image": "https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=600"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon": "Wine",
|
|
||||||
"title": "Premium Bar",
|
|
||||||
"description": "Extensive wine collection and craft cocktails in elegant settings",
|
|
||||||
"image": "https://images.unsplash.com/photo-1514362545857-3bc16c4c7d1b?w=600"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon": "Dumbbell",
|
|
||||||
"title": "Spa & Wellness",
|
|
||||||
"description": "Rejuvenating spa treatments and state-of-the-art fitness center",
|
|
||||||
"image": "https://images.unsplash.com/photo-1544161515-4ab6ce6db874?w=600"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon": "Car",
|
|
||||||
"title": "Concierge Services",
|
|
||||||
"description": "Personalized assistance for all your travel and entertainment needs",
|
|
||||||
"image": "https://images.unsplash.com/photo-1558618666-fcd25c85cd64?w=600"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# Luxury Experiences
|
|
||||||
luxury_experiences = [
|
|
||||||
{
|
|
||||||
"icon": "Sunset",
|
|
||||||
"title": "Sunset Rooftop",
|
|
||||||
"description": "Breathtaking views and exclusive rooftop experiences",
|
|
||||||
"image": "https://images.unsplash.com/photo-1514933651103-005eec06c04b?w=600"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon": "Ship",
|
|
||||||
"title": "Yacht Excursions",
|
|
||||||
"description": "Private yacht charters for unforgettable sea adventures",
|
|
||||||
"image": "https://images.unsplash.com/photo-1544551763-46a013bb70d5?w=600"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon": "Music",
|
|
||||||
"title": "Live Entertainment",
|
|
||||||
"description": "World-class performances and exclusive events",
|
|
||||||
"image": "https://images.unsplash.com/photo-1470229722913-7c0e2dbbafd3?w=600"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon": "Palette",
|
|
||||||
"title": "Art & Culture",
|
|
||||||
"description": "Curated art collections and cultural experiences",
|
|
||||||
"image": "https://images.unsplash.com/photo-1578301978018-3005759f48f7?w=600"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# Awards
|
|
||||||
awards = [
|
|
||||||
{
|
|
||||||
"icon": "Trophy",
|
|
||||||
"title": "Best Luxury Hotel 2024",
|
|
||||||
"description": "Awarded by International Luxury Travel Association",
|
|
||||||
"image": "https://images.unsplash.com/photo-1579783902614-a3fb3927b6a5?w=400",
|
|
||||||
"year": "2024"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon": "Star",
|
|
||||||
"title": "5-Star Excellence",
|
|
||||||
"description": "Consistently rated 5 stars by leading travel publications",
|
|
||||||
"image": "https://images.unsplash.com/photo-1606761568499-6d2451b23c66?w=400",
|
|
||||||
"year": "2023"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon": "Award",
|
|
||||||
"title": "Sustainable Luxury",
|
|
||||||
"description": "Recognized for environmental responsibility and sustainability",
|
|
||||||
"image": "https://images.unsplash.com/photo-1473341304170-971dccb5ac1e?w=400",
|
|
||||||
"year": "2024"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# Partners
|
|
||||||
partners = [
|
|
||||||
{
|
|
||||||
"name": "Luxury Travel Group",
|
|
||||||
"logo": "https://images.unsplash.com/photo-1599305445671-ac291c95aaa9?w=200",
|
|
||||||
"link": "#"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Premium Airlines",
|
|
||||||
"logo": "https://images.unsplash.com/photo-1436491865332-7a61a109cc05?w=200",
|
|
||||||
"link": "#"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Exclusive Events",
|
|
||||||
"logo": "https://images.unsplash.com/photo-1511578314322-379afb476865?w=200",
|
|
||||||
"link": "#"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Fine Dining Network",
|
|
||||||
"logo": "https://images.unsplash.com/photo-1555396273-367ea4eb4db5?w=200",
|
|
||||||
"link": "#"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# Stats
|
|
||||||
stats = [
|
|
||||||
{
|
|
||||||
"icon": "Users",
|
|
||||||
"number": "50,000+",
|
|
||||||
"label": "Happy Guests"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon": "Award",
|
|
||||||
"number": "25+",
|
|
||||||
"label": "Awards Won"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon": "Star",
|
|
||||||
"number": "4.9",
|
|
||||||
"label": "Average Rating"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon": "Globe",
|
|
||||||
"number": "100+",
|
|
||||||
"label": "Countries Served"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# Amenities
|
|
||||||
amenities = [
|
|
||||||
{
|
|
||||||
"icon": "Wifi",
|
|
||||||
"title": "High-Speed WiFi",
|
|
||||||
"description": "Complimentary high-speed internet throughout the property",
|
|
||||||
"image": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon": "Coffee",
|
|
||||||
"title": "24/7 Room Service",
|
|
||||||
"description": "Round-the-clock dining and beverage service",
|
|
||||||
"image": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon": "Car",
|
|
||||||
"title": "Valet Parking",
|
|
||||||
"description": "Complimentary valet parking for all guests",
|
|
||||||
"image": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon": "Plane",
|
|
||||||
"title": "Airport Transfer",
|
|
||||||
"description": "Luxury airport transfer service available",
|
|
||||||
"image": ""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# Testimonials
|
|
||||||
testimonials = [
|
|
||||||
{
|
|
||||||
"name": "Robert Martinez",
|
|
||||||
"role": "CEO, Tech Corp",
|
|
||||||
"image": "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?w=200",
|
|
||||||
"rating": 5,
|
|
||||||
"comment": "Exceptional service and attention to detail. The staff went above and beyond to make our stay memorable."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Lisa Anderson",
|
|
||||||
"role": "Travel Blogger",
|
|
||||||
"image": "https://images.unsplash.com/photo-1487412720507-e7ab37603c6f?w=200",
|
|
||||||
"rating": 5,
|
|
||||||
"comment": "The most luxurious hotel experience I've ever had. Every detail was perfect."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "David Thompson",
|
|
||||||
"role": "Investment Banker",
|
|
||||||
"image": "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=200",
|
|
||||||
"rating": 5,
|
|
||||||
"comment": "Outstanding facilities and impeccable service. Highly recommend for business travelers."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# Gallery Images
|
|
||||||
gallery_images = [
|
|
||||||
"https://images.unsplash.com/photo-1564501049412-61c2a3083791?w=800",
|
|
||||||
"https://images.unsplash.com/photo-1590490360182-c33d57733427?w=800",
|
|
||||||
"https://images.unsplash.com/photo-1551882547-ff40c63fe5fa?w=800",
|
|
||||||
"https://images.unsplash.com/photo-1571896349842-33c89424de2d?w=800",
|
|
||||||
"https://images.unsplash.com/photo-1582719478250-c89cae4dc85b?w=800",
|
|
||||||
"https://images.unsplash.com/photo-1566073771259-6a8506099945?w=800"
|
|
||||||
]
|
|
||||||
|
|
||||||
homepage_data = {
|
|
||||||
"page_type": PageType.HOME,
|
|
||||||
"title": "Luxury Hotel - Experience Unparalleled Elegance",
|
|
||||||
"subtitle": "Where timeless luxury meets modern sophistication",
|
|
||||||
"description": "Discover a world of refined elegance and exceptional service",
|
|
||||||
"hero_title": "Welcome to Luxury",
|
|
||||||
"hero_subtitle": "Experience the pinnacle of hospitality",
|
|
||||||
"hero_image": "https://images.unsplash.com/photo-1564501049412-61c2a3083791?w=1200",
|
|
||||||
"luxury_section_title": "Experience Unparalleled Luxury",
|
|
||||||
"luxury_section_subtitle": "Where elegance meets comfort in every detail",
|
|
||||||
"luxury_section_image": "https://images.unsplash.com/photo-1571896349842-33c89424de2d?w=1200",
|
|
||||||
"luxury_features": json.dumps(luxury_features),
|
|
||||||
"luxury_gallery_section_title": "Our Luxury Gallery",
|
|
||||||
"luxury_gallery_section_subtitle": "A glimpse into our world of elegance",
|
|
||||||
"luxury_gallery": json.dumps(luxury_gallery),
|
|
||||||
"luxury_testimonials_section_title": "What Our Guests Say",
|
|
||||||
"luxury_testimonials_section_subtitle": "Testimonials from our valued guests",
|
|
||||||
"luxury_testimonials": json.dumps(luxury_testimonials),
|
|
||||||
"luxury_services_section_title": "Premium Services",
|
|
||||||
"luxury_services_section_subtitle": "Indulge in our world-class amenities",
|
|
||||||
"luxury_services": json.dumps(luxury_services),
|
|
||||||
"luxury_experiences_section_title": "Exclusive Experiences",
|
|
||||||
"luxury_experiences_section_subtitle": "Create unforgettable memories",
|
|
||||||
"luxury_experiences": json.dumps(luxury_experiences),
|
|
||||||
"awards_section_title": "Awards & Recognition",
|
|
||||||
"awards_section_subtitle": "Recognized for excellence worldwide",
|
|
||||||
"awards": json.dumps(awards),
|
|
||||||
"partners_section_title": "Our Partners",
|
|
||||||
"partners_section_subtitle": "Trusted by leading brands",
|
|
||||||
"partners": json.dumps(partners),
|
|
||||||
"amenities_section_title": "Premium Amenities",
|
|
||||||
"amenities_section_subtitle": "Everything you need for a perfect stay",
|
|
||||||
"amenities": json.dumps(amenities),
|
|
||||||
"testimonials_section_title": "Guest Reviews",
|
|
||||||
"testimonials_section_subtitle": "Hear from our satisfied guests",
|
|
||||||
"testimonials": json.dumps(testimonials),
|
|
||||||
"gallery_section_title": "Photo Gallery",
|
|
||||||
"gallery_section_subtitle": "Explore our beautiful spaces",
|
|
||||||
"gallery_images": json.dumps(gallery_images),
|
|
||||||
"about_preview_title": "About Our Luxury Hotel",
|
|
||||||
"about_preview_subtitle": "A legacy of excellence",
|
|
||||||
"about_preview_content": "Discover a world of refined elegance and exceptional service. Our hotel combines timeless luxury with modern amenities to create an unforgettable experience. With over 50,000 satisfied guests and numerous awards, we continue to set the standard for luxury hospitality.",
|
|
||||||
"about_preview_image": "https://images.unsplash.com/photo-1582719478250-c89cae4dc85b?w=800",
|
|
||||||
"stats": json.dumps(stats),
|
|
||||||
"cta_title": "Ready to Experience Luxury?",
|
|
||||||
"cta_subtitle": "Book your stay today and discover the difference",
|
|
||||||
"cta_button_text": "Book Now",
|
|
||||||
"cta_button_link": "/rooms",
|
|
||||||
"cta_image": "https://images.unsplash.com/photo-1566073771259-6a8506099945?w=1200",
|
|
||||||
"is_active": True
|
|
||||||
}
|
|
||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
for key, value in homepage_data.items():
|
for key, value in homepage_data.items():
|
||||||
if key != "page_type":
|
if key != 'page_type':
|
||||||
setattr(existing, key, value)
|
setattr(existing, key, value)
|
||||||
print("✓ Updated existing homepage content")
|
print('✓ Updated existing homepage content')
|
||||||
else:
|
else:
|
||||||
new_content = PageContent(**homepage_data)
|
new_content = PageContent(**homepage_data)
|
||||||
db.add(new_content)
|
db.add(new_content)
|
||||||
print("✓ Created new homepage content")
|
print('✓ Created new homepage content')
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
def seed_footer_content(db: Session):
|
def seed_footer_content(db: Session):
|
||||||
"""Seed comprehensive footer content"""
|
|
||||||
existing = db.query(PageContent).filter(PageContent.page_type == PageType.FOOTER).first()
|
existing = db.query(PageContent).filter(PageContent.page_type == PageType.FOOTER).first()
|
||||||
|
contact_info = {'phone': '+1 (555) 123-4567', 'email': 'info@luxuryhotel.com', 'address': '123 Luxury Avenue, Premium District, City 12345'}
|
||||||
# Contact Info
|
social_links = {'facebook': 'https://facebook.com/luxuryhotel', 'twitter': 'https://twitter.com/luxuryhotel', 'instagram': 'https://instagram.com/luxuryhotel', 'linkedin': 'https://linkedin.com/company/luxuryhotel', 'youtube': 'https://youtube.com/luxuryhotel'}
|
||||||
contact_info = {
|
footer_links = {'quick_links': [{'label': 'Home', 'url': '/'}, {'label': 'Rooms & Suites', 'url': '/rooms'}, {'label': 'About Us', 'url': '/about'}, {'label': 'Contact', 'url': '/contact'}, {'label': 'Gallery', 'url': '/gallery'}], 'support_links': [{'label': 'FAQ', 'url': '/faq'}, {'label': 'Privacy Policy', 'url': '/privacy'}, {'label': 'Terms of Service', 'url': '/terms'}, {'label': 'Cancellation Policy', 'url': '/cancellation'}, {'label': 'Accessibility', 'url': '/accessibility'}]}
|
||||||
"phone": "+1 (555) 123-4567",
|
badges = [{'text': '5-Star Rated', 'icon': 'Star'}, {'text': 'Award Winning', 'icon': 'Award'}, {'text': 'Eco Certified', 'icon': 'Leaf'}, {'text': 'Luxury Collection', 'icon': 'Crown'}]
|
||||||
"email": "info@luxuryhotel.com",
|
footer_data = {'page_type': PageType.FOOTER, 'title': 'Luxury Hotel', 'subtitle': 'Experience Unparalleled Elegance', 'description': 'Your gateway to luxury hospitality and exceptional service', 'contact_info': json.dumps(contact_info), 'social_links': json.dumps(social_links), 'footer_links': json.dumps(footer_links), 'badges': json.dumps(badges), 'copyright_text': '© {YEAR} Luxury Hotel. All rights reserved.', 'is_active': True}
|
||||||
"address": "123 Luxury Avenue, Premium District, City 12345"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Social Links
|
|
||||||
social_links = {
|
|
||||||
"facebook": "https://facebook.com/luxuryhotel",
|
|
||||||
"twitter": "https://twitter.com/luxuryhotel",
|
|
||||||
"instagram": "https://instagram.com/luxuryhotel",
|
|
||||||
"linkedin": "https://linkedin.com/company/luxuryhotel",
|
|
||||||
"youtube": "https://youtube.com/luxuryhotel"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Footer Links
|
|
||||||
footer_links = {
|
|
||||||
"quick_links": [
|
|
||||||
{"label": "Home", "url": "/"},
|
|
||||||
{"label": "Rooms & Suites", "url": "/rooms"},
|
|
||||||
{"label": "About Us", "url": "/about"},
|
|
||||||
{"label": "Contact", "url": "/contact"},
|
|
||||||
{"label": "Gallery", "url": "/gallery"}
|
|
||||||
],
|
|
||||||
"support_links": [
|
|
||||||
{"label": "FAQ", "url": "/faq"},
|
|
||||||
{"label": "Privacy Policy", "url": "/privacy"},
|
|
||||||
{"label": "Terms of Service", "url": "/terms"},
|
|
||||||
{"label": "Cancellation Policy", "url": "/cancellation"},
|
|
||||||
{"label": "Accessibility", "url": "/accessibility"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Badges
|
|
||||||
badges = [
|
|
||||||
{
|
|
||||||
"text": "5-Star Rated",
|
|
||||||
"icon": "Star"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "Award Winning",
|
|
||||||
"icon": "Award"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "Eco Certified",
|
|
||||||
"icon": "Leaf"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "Luxury Collection",
|
|
||||||
"icon": "Crown"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
footer_data = {
|
|
||||||
"page_type": PageType.FOOTER,
|
|
||||||
"title": "Luxury Hotel",
|
|
||||||
"subtitle": "Experience Unparalleled Elegance",
|
|
||||||
"description": "Your gateway to luxury hospitality and exceptional service",
|
|
||||||
"contact_info": json.dumps(contact_info),
|
|
||||||
"social_links": json.dumps(social_links),
|
|
||||||
"footer_links": json.dumps(footer_links),
|
|
||||||
"badges": json.dumps(badges),
|
|
||||||
"copyright_text": "© {YEAR} Luxury Hotel. All rights reserved.",
|
|
||||||
"is_active": True
|
|
||||||
}
|
|
||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
for key, value in footer_data.items():
|
for key, value in footer_data.items():
|
||||||
if key != "page_type":
|
if key != 'page_type':
|
||||||
setattr(existing, key, value)
|
setattr(existing, key, value)
|
||||||
print("✓ Updated existing footer content")
|
print('✓ Updated existing footer content')
|
||||||
else:
|
else:
|
||||||
new_content = PageContent(**footer_data)
|
new_content = PageContent(**footer_data)
|
||||||
db.add(new_content)
|
db.add(new_content)
|
||||||
print("✓ Created new footer content")
|
print('✓ Created new footer content')
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Main seed function"""
|
|
||||||
db: Session = SessionLocal()
|
db: Session = SessionLocal()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
print("=" * 80)
|
print('=' * 80)
|
||||||
print("SEEDING HOMEPAGE AND FOOTER CONTENT")
|
print('SEEDING HOMEPAGE AND FOOTER CONTENT')
|
||||||
print("=" * 80)
|
print('=' * 80)
|
||||||
print()
|
print()
|
||||||
|
print('Seeding homepage content...')
|
||||||
print("Seeding homepage content...")
|
|
||||||
seed_homepage_content(db)
|
seed_homepage_content(db)
|
||||||
|
print('\nSeeding footer content...')
|
||||||
print("\nSeeding footer content...")
|
|
||||||
seed_footer_content(db)
|
seed_footer_content(db)
|
||||||
|
print('\n' + '=' * 80)
|
||||||
print("\n" + "=" * 80)
|
print('✓ All content seeded successfully!')
|
||||||
print("✓ All content seeded successfully!")
|
print('=' * 80)
|
||||||
print("=" * 80)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
print(f"\n✗ Error seeding content: {e}")
|
print(f'\n✗ Error seeding content: {e}')
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
if __name__ == '__main__':
|
||||||
if __name__ == "__main__":
|
main()
|
||||||
main()
|
|
||||||
|
|
||||||
@@ -1,123 +1,42 @@
|
|||||||
"""
|
|
||||||
Seed script to populate initial luxury content for the homepage.
|
|
||||||
Run this script to add default luxury content to the page_content table.
|
|
||||||
"""
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
|
||||||
# Add the src directory to the path
|
|
||||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from src.config.database import SessionLocal, engine
|
from src.config.database import SessionLocal, engine
|
||||||
from src.models.page_content import PageContent
|
from src.models.page_content import PageContent
|
||||||
from src.models.user import User
|
from src.models.user import User
|
||||||
|
|
||||||
def seed_luxury_content():
|
def seed_luxury_content():
|
||||||
"""Seed luxury content for the homepage"""
|
|
||||||
db: Session = SessionLocal()
|
db: Session = SessionLocal()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Check if home page content already exists
|
|
||||||
existing = db.query(PageContent).filter(PageContent.page_type == 'home').first()
|
existing = db.query(PageContent).filter(PageContent.page_type == 'home').first()
|
||||||
|
luxury_features = [{'icon': 'Sparkles', 'title': 'Premium Amenities', 'description': 'World-class facilities designed for your comfort and relaxation'}, {'icon': 'Crown', 'title': 'Royal Service', 'description': 'Dedicated concierge service available 24/7 for all your needs'}, {'icon': 'Award', 'title': 'Award-Winning', 'description': 'Recognized for excellence in hospitality and guest satisfaction'}, {'icon': 'Shield', 'title': 'Secure & Private', 'description': 'Your privacy and security are our top priorities'}, {'icon': 'Heart', 'title': 'Personalized Care', 'description': 'Tailored experiences crafted just for you'}, {'icon': 'Gem', 'title': 'Luxury Design', 'description': 'Elegantly designed spaces with attention to every detail'}]
|
||||||
luxury_features = [
|
luxury_testimonials = [{'name': 'Sarah Johnson', 'title': 'Business Executive', 'quote': 'An absolutely stunning experience. The attention to detail and level of service exceeded all expectations.', 'image': ''}, {'name': 'Michael Chen', 'title': 'Travel Enthusiast', 'quote': 'The epitome of luxury. Every moment was perfect, from check-in to check-out.', 'image': ''}, {'name': 'Emma Williams', 'title': 'Luxury Traveler', 'quote': 'This hotel redefines what luxury means. I will definitely return.', 'image': ''}]
|
||||||
{
|
|
||||||
"icon": "Sparkles",
|
|
||||||
"title": "Premium Amenities",
|
|
||||||
"description": "World-class facilities designed for your comfort and relaxation"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon": "Crown",
|
|
||||||
"title": "Royal Service",
|
|
||||||
"description": "Dedicated concierge service available 24/7 for all your needs"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon": "Award",
|
|
||||||
"title": "Award-Winning",
|
|
||||||
"description": "Recognized for excellence in hospitality and guest satisfaction"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon": "Shield",
|
|
||||||
"title": "Secure & Private",
|
|
||||||
"description": "Your privacy and security are our top priorities"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon": "Heart",
|
|
||||||
"title": "Personalized Care",
|
|
||||||
"description": "Tailored experiences crafted just for you"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"icon": "Gem",
|
|
||||||
"title": "Luxury Design",
|
|
||||||
"description": "Elegantly designed spaces with attention to every detail"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
luxury_testimonials = [
|
|
||||||
{
|
|
||||||
"name": "Sarah Johnson",
|
|
||||||
"title": "Business Executive",
|
|
||||||
"quote": "An absolutely stunning experience. The attention to detail and level of service exceeded all expectations.",
|
|
||||||
"image": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Michael Chen",
|
|
||||||
"title": "Travel Enthusiast",
|
|
||||||
"quote": "The epitome of luxury. Every moment was perfect, from check-in to check-out.",
|
|
||||||
"image": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Emma Williams",
|
|
||||||
"title": "Luxury Traveler",
|
|
||||||
"quote": "This hotel redefines what luxury means. I will definitely return.",
|
|
||||||
"image": ""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
# Update existing content
|
existing.luxury_section_title = 'Experience Unparalleled Luxury'
|
||||||
existing.luxury_section_title = "Experience Unparalleled Luxury"
|
existing.luxury_section_subtitle = 'Where elegance meets comfort in every detail'
|
||||||
existing.luxury_section_subtitle = "Where elegance meets comfort in every detail"
|
|
||||||
existing.luxury_section_image = None
|
existing.luxury_section_image = None
|
||||||
existing.luxury_features = json.dumps(luxury_features)
|
existing.luxury_features = json.dumps(luxury_features)
|
||||||
existing.luxury_gallery = json.dumps([])
|
existing.luxury_gallery = json.dumps([])
|
||||||
existing.luxury_testimonials = json.dumps(luxury_testimonials)
|
existing.luxury_testimonials = json.dumps(luxury_testimonials)
|
||||||
existing.about_preview_title = "About Our Luxury Hotel"
|
existing.about_preview_title = 'About Our Luxury Hotel'
|
||||||
existing.about_preview_content = "Discover a world of refined elegance and exceptional service. Our hotel combines timeless luxury with modern amenities to create an unforgettable experience."
|
existing.about_preview_content = 'Discover a world of refined elegance and exceptional service. Our hotel combines timeless luxury with modern amenities to create an unforgettable experience.'
|
||||||
existing.about_preview_image = None
|
existing.about_preview_image = None
|
||||||
print("✓ Updated existing home page content with luxury sections")
|
print('✓ Updated existing home page content with luxury sections')
|
||||||
else:
|
else:
|
||||||
# Create new content
|
new_content = PageContent(page_type='home', luxury_section_title='Experience Unparalleled Luxury', luxury_section_subtitle='Where elegance meets comfort in every detail', luxury_section_image=None, luxury_features=json.dumps(luxury_features), luxury_gallery=json.dumps([]), luxury_testimonials=json.dumps(luxury_testimonials), about_preview_title='About Our Luxury Hotel', about_preview_content='Discover a world of refined elegance and exceptional service. Our hotel combines timeless luxury with modern amenities to create an unforgettable experience.', about_preview_image=None, is_active=True)
|
||||||
new_content = PageContent(
|
|
||||||
page_type='home',
|
|
||||||
luxury_section_title="Experience Unparalleled Luxury",
|
|
||||||
luxury_section_subtitle="Where elegance meets comfort in every detail",
|
|
||||||
luxury_section_image=None,
|
|
||||||
luxury_features=json.dumps(luxury_features),
|
|
||||||
luxury_gallery=json.dumps([]),
|
|
||||||
luxury_testimonials=json.dumps(luxury_testimonials),
|
|
||||||
about_preview_title="About Our Luxury Hotel",
|
|
||||||
about_preview_content="Discover a world of refined elegance and exceptional service. Our hotel combines timeless luxury with modern amenities to create an unforgettable experience.",
|
|
||||||
about_preview_image=None,
|
|
||||||
is_active=True
|
|
||||||
)
|
|
||||||
db.add(new_content)
|
db.add(new_content)
|
||||||
print("✓ Created new home page content with luxury sections")
|
print('✓ Created new home page content with luxury sections')
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
print("✓ Luxury content seeded successfully!")
|
print('✓ Luxury content seeded successfully!')
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
print(f"✗ Error seeding luxury content: {e}")
|
print(f'✗ Error seeding luxury content: {e}')
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
if __name__ == '__main__':
|
||||||
if __name__ == "__main__":
|
print('Seeding luxury content...')
|
||||||
print("Seeding luxury content...")
|
|
||||||
seed_luxury_content()
|
seed_luxury_content()
|
||||||
print("Done!")
|
print('Done!')
|
||||||
|
|
||||||
@@ -1,15 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Seed script to delete all existing rooms and create 50 sample luxury hotel rooms
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Add the parent directory to the path so we can import from src
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent))
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from src.config.database import SessionLocal, engine
|
from src.config.database import SessionLocal, engine
|
||||||
from src.models.room import Room, RoomStatus
|
from src.models.room import Room, RoomStatus
|
||||||
@@ -19,7 +11,6 @@ import json
|
|||||||
import random
|
import random
|
||||||
|
|
||||||
def get_db():
|
def get_db():
|
||||||
"""Get database session"""
|
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
return db
|
return db
|
||||||
@@ -27,169 +18,60 @@ def get_db():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def seed_rooms(db: Session):
|
def seed_rooms(db: Session):
|
||||||
"""Delete all existing rooms and create 50 sample luxury rooms"""
|
print('=' * 80)
|
||||||
print("=" * 80)
|
print('SEEDING ROOMS - DELETING EXISTING AND CREATING 50 NEW LUXURY ROOMS')
|
||||||
print("SEEDING ROOMS - DELETING EXISTING AND CREATING 50 NEW LUXURY ROOMS")
|
print('=' * 80)
|
||||||
print("=" * 80)
|
|
||||||
|
|
||||||
# Get all room types
|
|
||||||
room_types = db.query(RoomType).all()
|
room_types = db.query(RoomType).all()
|
||||||
if not room_types:
|
if not room_types:
|
||||||
print("❌ No room types found! Please create room types first.")
|
print('❌ No room types found! Please create room types first.')
|
||||||
return
|
return
|
||||||
|
print(f'\n✓ Found {len(room_types)} room type(s)')
|
||||||
print(f"\n✓ Found {len(room_types)} room type(s)")
|
|
||||||
for rt in room_types:
|
for rt in room_types:
|
||||||
print(f" - {rt.name} (ID: {rt.id}, Base Price: {rt.base_price})")
|
print(f' - {rt.name} (ID: {rt.id}, Base Price: {rt.base_price})')
|
||||||
|
|
||||||
# Delete all existing rooms
|
|
||||||
# First, we need to handle related records that reference these rooms
|
|
||||||
from src.models.booking import Booking
|
from src.models.booking import Booking
|
||||||
from src.models.review import Review
|
from src.models.review import Review
|
||||||
from src.models.favorite import Favorite
|
from src.models.favorite import Favorite
|
||||||
|
|
||||||
existing_rooms = db.query(Room).all()
|
existing_rooms = db.query(Room).all()
|
||||||
if existing_rooms:
|
if existing_rooms:
|
||||||
print(f"\n🗑️ Deleting {len(existing_rooms)} existing room(s)...")
|
print(f'\n🗑️ Deleting {len(existing_rooms)} existing room(s)...')
|
||||||
|
|
||||||
# Get all room IDs
|
|
||||||
room_ids = [room.id for room in existing_rooms]
|
room_ids = [room.id for room in existing_rooms]
|
||||||
|
|
||||||
# Delete bookings that reference these rooms
|
|
||||||
bookings_with_rooms = db.query(Booking).filter(Booking.room_id.in_(room_ids)).all()
|
bookings_with_rooms = db.query(Booking).filter(Booking.room_id.in_(room_ids)).all()
|
||||||
if bookings_with_rooms:
|
if bookings_with_rooms:
|
||||||
print(f" ⚠️ Found {len(bookings_with_rooms)} booking(s) referencing these rooms")
|
print(f' ⚠️ Found {len(bookings_with_rooms)} booking(s) referencing these rooms')
|
||||||
for booking in bookings_with_rooms:
|
for booking in bookings_with_rooms:
|
||||||
db.delete(booking)
|
db.delete(booking)
|
||||||
print(f" ✓ Deleted {len(bookings_with_rooms)} booking(s)")
|
print(f' ✓ Deleted {len(bookings_with_rooms)} booking(s)')
|
||||||
|
|
||||||
# Delete reviews that reference these rooms
|
|
||||||
reviews_with_rooms = db.query(Review).filter(Review.room_id.in_(room_ids)).all()
|
reviews_with_rooms = db.query(Review).filter(Review.room_id.in_(room_ids)).all()
|
||||||
if reviews_with_rooms:
|
if reviews_with_rooms:
|
||||||
print(f" ⚠️ Found {len(reviews_with_rooms)} review(s) referencing these rooms")
|
print(f' ⚠️ Found {len(reviews_with_rooms)} review(s) referencing these rooms')
|
||||||
for review in reviews_with_rooms:
|
for review in reviews_with_rooms:
|
||||||
db.delete(review)
|
db.delete(review)
|
||||||
print(f" ✓ Deleted {len(reviews_with_rooms)} review(s)")
|
print(f' ✓ Deleted {len(reviews_with_rooms)} review(s)')
|
||||||
|
|
||||||
# Delete favorites that reference these rooms
|
|
||||||
favorites_with_rooms = db.query(Favorite).filter(Favorite.room_id.in_(room_ids)).all()
|
favorites_with_rooms = db.query(Favorite).filter(Favorite.room_id.in_(room_ids)).all()
|
||||||
if favorites_with_rooms:
|
if favorites_with_rooms:
|
||||||
print(f" ⚠️ Found {len(favorites_with_rooms)} favorite(s) referencing these rooms")
|
print(f' ⚠️ Found {len(favorites_with_rooms)} favorite(s) referencing these rooms')
|
||||||
for favorite in favorites_with_rooms:
|
for favorite in favorites_with_rooms:
|
||||||
db.delete(favorite)
|
db.delete(favorite)
|
||||||
print(f" ✓ Deleted {len(favorites_with_rooms)} favorite(s)")
|
print(f' ✓ Deleted {len(favorites_with_rooms)} favorite(s)')
|
||||||
|
|
||||||
# Now delete the rooms
|
|
||||||
for room in existing_rooms:
|
for room in existing_rooms:
|
||||||
db.delete(room)
|
db.delete(room)
|
||||||
db.commit()
|
db.commit()
|
||||||
print(f"✓ Deleted {len(existing_rooms)} room(s)")
|
print(f'✓ Deleted {len(existing_rooms)} room(s)')
|
||||||
|
views = ['Ocean View', 'City View', 'Garden View', 'Mountain View', 'Pool View', 'Beach View', 'Panoramic View', 'Sea View']
|
||||||
# Luxury room configurations
|
room_sizes = ['35 sqm', '40 sqm', '45 sqm', '50 sqm', '55 sqm', '60 sqm', '70 sqm', '80 sqm', '90 sqm', '100 sqm', '120 sqm', '150 sqm', '180 sqm', '200 sqm', '250 sqm']
|
||||||
views = [
|
luxury_room_images = ['https://images.unsplash.com/photo-1631049307264-da0ec9d70304?w=1200&h=800&fit=crop', 'https://images.unsplash.com/photo-1611892440504-42a792e24d32?w=1200&h=800&fit=crop', 'https://images.unsplash.com/photo-1590490360182-c33d57733427?w=1200&h=800&fit=crop', 'https://images.unsplash.com/photo-1564501049412-61c2a3083791?w=1200&h=800&fit=crop', 'https://images.unsplash.com/photo-1578683010236-d716f9a3f461?w=1200&h=800&fit=crop', 'https://images.unsplash.com/photo-1582719478250-c89cae4dc85b?w=1200&h=800&fit=crop', 'https://images.unsplash.com/photo-1596394516093-501ba68a0ba6?w=1200&h=800&fit=crop', 'https://images.unsplash.com/photo-1618221195710-dd6b41faaea8?w=1200&h=800&fit=crop', 'https://images.unsplash.com/photo-1566665797739-1674de7a421a?w=1200&h=800&fit=crop', 'https://images.unsplash.com/photo-1566073771259-6a8506099945?w=1200&h=800&fit=crop', 'https://images.unsplash.com/photo-1618773928121-c32242e63f39?w=1200&h=800&fit=crop', 'https://images.unsplash.com/photo-1582719508461-905c673771fd?w=1200&h=800&fit=crop', 'https://images.unsplash.com/photo-1595576508898-0ad5c879a061?w=1200&h=800&fit=crop', 'https://images.unsplash.com/photo-1571003123894-1f0594d2b5d9?w=1200&h=800&fit=crop', 'https://images.unsplash.com/photo-1566073771259-6a8506099945?w=1200&h=800&fit=crop', 'https://images.unsplash.com/photo-1578683010236-d716f9a3f461?w=1200&h=800&fit=crop', 'https://images.unsplash.com/photo-1596394516093-501ba68a0ba6?w=1200&h=800&fit=crop', 'https://images.unsplash.com/photo-1631049307264-da0ec9d70304?w=1200&h=800&fit=crop', 'https://images.unsplash.com/photo-1611892440504-42a792e24d32?w=1200&h=800&fit=crop', 'https://images.unsplash.com/photo-1590490360182-c33d57733427?w=1200&h=800&fit=crop', 'https://images.unsplash.com/photo-1564501049412-61c2a3083791?w=1200&h=800&fit=crop', 'https://images.unsplash.com/photo-1582719478250-c89cae4dc85b?w=1200&h=800&fit=crop', 'https://images.unsplash.com/photo-1618221195710-dd6b41faaea8?w=1200&h=800&fit=crop', 'https://images.unsplash.com/photo-1566665797739-1674de7a421a?w=1200&h=800&fit=crop', 'https://images.unsplash.com/photo-1618773928121-c32242e63f39?w=1200&h=800&fit=crop', 'https://images.unsplash.com/photo-1582719508461-905c673771fd?w=1200&h=800&fit=crop', 'https://images.unsplash.com/photo-1595576508898-0ad5c879a061?w=1200&h=800&fit=crop', 'https://images.unsplash.com/photo-1571003123894-1f0594d2b5d9?w=1200&h=800&fit=crop']
|
||||||
"Ocean View", "City View", "Garden View", "Mountain View",
|
all_amenities = ['Free WiFi', 'High-Speed Internet', 'Smart TV', 'Netflix', 'Air Conditioning', 'Climate Control', 'Private Balcony', 'Ocean View', 'City View', 'Minibar', 'Coffee Maker', 'Espresso Machine', 'Refrigerator', 'Safe', 'Iron & Ironing Board', 'Hair Dryer', 'Premium Toiletries', 'Bathrobes', 'Slippers', 'Work Desk', 'Ergonomic Chair', 'USB Charging Ports', 'Bluetooth Speaker', 'Sound System', 'Blackout Curtains', 'Pillow Menu', 'Turndown Service', 'Room Service', '24/7 Concierge']
|
||||||
"Pool View", "Beach View", "Panoramic View", "Sea View"
|
premium_amenities = ['Jacuzzi Bathtub', 'Steam Shower', 'Rain Shower', 'Bidet', 'Private Pool', 'Outdoor Terrace', 'Fireplace', 'Wine Cellar', 'Private Bar', 'Butler Service', 'Private Elevator', 'Helipad Access']
|
||||||
]
|
room_configs = [((1, 3), 8, 'Garden View', (35, 45), 8, False), ((1, 3), 8, 'City View', (40, 50), 9, False), ((4, 6), 6, 'City View', (45, 55), 10, False), ((4, 6), 6, 'Pool View', (50, 60), 11, True), ((7, 9), 5, 'Ocean View', (55, 70), 12, True), ((7, 9), 5, 'Mountain View', (60, 75), 12, False), ((10, 12), 4, 'Panoramic View', (80, 100), 14, True), ((10, 12), 4, 'Sea View', (90, 110), 15, True), ((13, 15), 3, 'Ocean View', (120, 150), 16, True), ((13, 15), 3, 'Beach View', (130, 160), 17, True), ((16, 16), 2, 'Panoramic View', (200, 250), 20, True)]
|
||||||
|
|
||||||
room_sizes = [
|
|
||||||
"35 sqm", "40 sqm", "45 sqm", "50 sqm", "55 sqm",
|
|
||||||
"60 sqm", "70 sqm", "80 sqm", "90 sqm", "100 sqm",
|
|
||||||
"120 sqm", "150 sqm", "180 sqm", "200 sqm", "250 sqm"
|
|
||||||
]
|
|
||||||
|
|
||||||
# Real luxury hotel room images from Unsplash (defined once, used for all rooms)
|
|
||||||
luxury_room_images = [
|
|
||||||
# Luxury hotel rooms
|
|
||||||
"https://images.unsplash.com/photo-1631049307264-da0ec9d70304?w=1200&h=800&fit=crop",
|
|
||||||
"https://images.unsplash.com/photo-1611892440504-42a792e24d32?w=1200&h=800&fit=crop",
|
|
||||||
"https://images.unsplash.com/photo-1590490360182-c33d57733427?w=1200&h=800&fit=crop",
|
|
||||||
"https://images.unsplash.com/photo-1564501049412-61c2a3083791?w=1200&h=800&fit=crop",
|
|
||||||
"https://images.unsplash.com/photo-1578683010236-d716f9a3f461?w=1200&h=800&fit=crop",
|
|
||||||
"https://images.unsplash.com/photo-1582719478250-c89cae4dc85b?w=1200&h=800&fit=crop",
|
|
||||||
"https://images.unsplash.com/photo-1596394516093-501ba68a0ba6?w=1200&h=800&fit=crop",
|
|
||||||
"https://images.unsplash.com/photo-1618221195710-dd6b41faaea8?w=1200&h=800&fit=crop",
|
|
||||||
"https://images.unsplash.com/photo-1566665797739-1674de7a421a?w=1200&h=800&fit=crop",
|
|
||||||
"https://images.unsplash.com/photo-1566073771259-6a8506099945?w=1200&h=800&fit=crop",
|
|
||||||
# Suite images
|
|
||||||
"https://images.unsplash.com/photo-1618773928121-c32242e63f39?w=1200&h=800&fit=crop",
|
|
||||||
"https://images.unsplash.com/photo-1582719508461-905c673771fd?w=1200&h=800&fit=crop",
|
|
||||||
"https://images.unsplash.com/photo-1595576508898-0ad5c879a061?w=1200&h=800&fit=crop",
|
|
||||||
# Additional luxury rooms
|
|
||||||
"https://images.unsplash.com/photo-1571003123894-1f0594d2b5d9?w=1200&h=800&fit=crop",
|
|
||||||
"https://images.unsplash.com/photo-1566073771259-6a8506099945?w=1200&h=800&fit=crop",
|
|
||||||
"https://images.unsplash.com/photo-1578683010236-d716f9a3f461?w=1200&h=800&fit=crop",
|
|
||||||
"https://images.unsplash.com/photo-1596394516093-501ba68a0ba6?w=1200&h=800&fit=crop",
|
|
||||||
"https://images.unsplash.com/photo-1631049307264-da0ec9d70304?w=1200&h=800&fit=crop",
|
|
||||||
"https://images.unsplash.com/photo-1611892440504-42a792e24d32?w=1200&h=800&fit=crop",
|
|
||||||
"https://images.unsplash.com/photo-1590490360182-c33d57733427?w=1200&h=800&fit=crop",
|
|
||||||
"https://images.unsplash.com/photo-1564501049412-61c2a3083791?w=1200&h=800&fit=crop",
|
|
||||||
"https://images.unsplash.com/photo-1582719478250-c89cae4dc85b?w=1200&h=800&fit=crop",
|
|
||||||
"https://images.unsplash.com/photo-1618221195710-dd6b41faaea8?w=1200&h=800&fit=crop",
|
|
||||||
"https://images.unsplash.com/photo-1566665797739-1674de7a421a?w=1200&h=800&fit=crop",
|
|
||||||
"https://images.unsplash.com/photo-1618773928121-c32242e63f39?w=1200&h=800&fit=crop",
|
|
||||||
"https://images.unsplash.com/photo-1582719508461-905c673771fd?w=1200&h=800&fit=crop",
|
|
||||||
"https://images.unsplash.com/photo-1595576508898-0ad5c879a061?w=1200&h=800&fit=crop",
|
|
||||||
"https://images.unsplash.com/photo-1571003123894-1f0594d2b5d9?w=1200&h=800&fit=crop",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Comprehensive luxury amenities
|
|
||||||
all_amenities = [
|
|
||||||
"Free WiFi", "High-Speed Internet", "Smart TV", "Netflix",
|
|
||||||
"Air Conditioning", "Climate Control", "Private Balcony",
|
|
||||||
"Ocean View", "City View", "Minibar", "Coffee Maker",
|
|
||||||
"Espresso Machine", "Refrigerator", "Safe", "Iron & Ironing Board",
|
|
||||||
"Hair Dryer", "Premium Toiletries", "Bathrobes", "Slippers",
|
|
||||||
"Work Desk", "Ergonomic Chair", "USB Charging Ports",
|
|
||||||
"Bluetooth Speaker", "Sound System", "Blackout Curtains",
|
|
||||||
"Pillow Menu", "Turndown Service", "Room Service", "24/7 Concierge"
|
|
||||||
]
|
|
||||||
|
|
||||||
premium_amenities = [
|
|
||||||
"Jacuzzi Bathtub", "Steam Shower", "Rain Shower", "Bidet",
|
|
||||||
"Private Pool", "Outdoor Terrace", "Fireplace", "Wine Cellar",
|
|
||||||
"Private Bar", "Butler Service", "Private Elevator", "Helipad Access"
|
|
||||||
]
|
|
||||||
|
|
||||||
# Room configurations: (floor_range, room_numbers_per_floor, view, size_range, amenities_count, featured)
|
|
||||||
room_configs = [
|
|
||||||
# Standard Rooms (Floors 1-3)
|
|
||||||
((1, 3), 8, "Garden View", (35, 45), 8, False),
|
|
||||||
((1, 3), 8, "City View", (40, 50), 9, False),
|
|
||||||
|
|
||||||
# Superior Rooms (Floors 4-6)
|
|
||||||
((4, 6), 6, "City View", (45, 55), 10, False),
|
|
||||||
((4, 6), 6, "Pool View", (50, 60), 11, True),
|
|
||||||
|
|
||||||
# Deluxe Rooms (Floors 7-9)
|
|
||||||
((7, 9), 5, "Ocean View", (55, 70), 12, True),
|
|
||||||
((7, 9), 5, "Mountain View", (60, 75), 12, False),
|
|
||||||
|
|
||||||
# Executive Suites (Floors 10-12)
|
|
||||||
((10, 12), 4, "Panoramic View", (80, 100), 14, True),
|
|
||||||
((10, 12), 4, "Sea View", (90, 110), 15, True),
|
|
||||||
|
|
||||||
# Luxury Suites (Floors 13-15)
|
|
||||||
((13, 15), 3, "Ocean View", (120, 150), 16, True),
|
|
||||||
((13, 15), 3, "Beach View", (130, 160), 17, True),
|
|
||||||
|
|
||||||
# Presidential Suites (Floor 16)
|
|
||||||
((16, 16), 2, "Panoramic View", (200, 250), 20, True),
|
|
||||||
]
|
|
||||||
|
|
||||||
rooms_created = []
|
rooms_created = []
|
||||||
room_counter = 101 # Starting room number
|
room_counter = 101
|
||||||
|
print(f'\n🏨 Creating 50 luxury rooms...\n')
|
||||||
print(f"\n🏨 Creating 50 luxury rooms...\n")
|
|
||||||
|
|
||||||
for config in room_configs:
|
for config in room_configs:
|
||||||
floor_range, rooms_per_floor, view_type, size_range, amenities_count, featured = config
|
floor_range, rooms_per_floor, view_type, size_range, amenities_count, featured = config
|
||||||
|
|
||||||
for floor in range(floor_range[0], floor_range[1] + 1):
|
for floor in range(floor_range[0], floor_range[1] + 1):
|
||||||
for _ in range(rooms_per_floor):
|
for _ in range(rooms_per_floor):
|
||||||
if len(rooms_created) >= 50:
|
if len(rooms_created) >= 50:
|
||||||
break
|
break
|
||||||
|
|
||||||
# Select random room type based on floor
|
|
||||||
if floor <= 3:
|
if floor <= 3:
|
||||||
room_type = random.choice([rt for rt in room_types if 'standard' in rt.name.lower() or 'superior' in rt.name.lower()] or room_types)
|
room_type = random.choice([rt for rt in room_types if 'standard' in rt.name.lower() or 'superior' in rt.name.lower()] or room_types)
|
||||||
elif floor <= 6:
|
elif floor <= 6:
|
||||||
@@ -200,118 +82,58 @@ def seed_rooms(db: Session):
|
|||||||
room_type = random.choice([rt for rt in room_types if 'executive' in rt.name.lower() or 'suite' in rt.name.lower()] or room_types)
|
room_type = random.choice([rt for rt in room_types if 'executive' in rt.name.lower() or 'suite' in rt.name.lower()] or room_types)
|
||||||
else:
|
else:
|
||||||
room_type = random.choice([rt for rt in room_types if 'suite' in rt.name.lower() or 'presidential' in rt.name.lower()] or room_types)
|
room_type = random.choice([rt for rt in room_types if 'suite' in rt.name.lower() or 'presidential' in rt.name.lower()] or room_types)
|
||||||
|
|
||||||
# If no matching room type, use random
|
|
||||||
if not room_type:
|
if not room_type:
|
||||||
room_type = random.choice(room_types)
|
room_type = random.choice(room_types)
|
||||||
|
|
||||||
# Calculate price (base price + floor premium + view premium + random variation)
|
|
||||||
base_price = float(room_type.base_price)
|
base_price = float(room_type.base_price)
|
||||||
floor_premium = (floor - 1) * 5 # +5 per floor
|
floor_premium = (floor - 1) * 5
|
||||||
view_premium = 20 if "Ocean" in view_type or "Sea" in view_type or "Beach" in view_type else 0
|
view_premium = 20 if 'Ocean' in view_type or 'Sea' in view_type or 'Beach' in view_type else 0
|
||||||
view_premium += 15 if "Panoramic" in view_type else 0
|
view_premium += 15 if 'Panoramic' in view_type else 0
|
||||||
view_premium += 10 if "Mountain" in view_type else 0
|
view_premium += 10 if 'Mountain' in view_type else 0
|
||||||
view_premium += 5 if "Pool" in view_type else 0
|
view_premium += 5 if 'Pool' in view_type else 0
|
||||||
# Add random variation (-5% to +10% of base price)
|
random_variation = base_price * random.uniform(-0.05, 0.1)
|
||||||
random_variation = base_price * random.uniform(-0.05, 0.10)
|
|
||||||
# Size premium (larger rooms cost more)
|
|
||||||
size_min, size_max = size_range
|
size_min, size_max = size_range
|
||||||
size_premium = (size_min + size_max) / 2 * 0.5 # ~0.5 per sqm
|
size_premium = (size_min + size_max) / 2 * 0.5
|
||||||
price = base_price + floor_premium + view_premium + random_variation + size_premium
|
price = base_price + floor_premium + view_premium + random_variation + size_premium
|
||||||
# Ensure minimum price and round to 2 decimal places
|
|
||||||
price = max(base_price * 0.95, price)
|
price = max(base_price * 0.95, price)
|
||||||
price = round(price, 2)
|
price = round(price, 2)
|
||||||
|
|
||||||
# Select amenities
|
|
||||||
selected_amenities = random.sample(all_amenities, min(amenities_count, len(all_amenities)))
|
selected_amenities = random.sample(all_amenities, min(amenities_count, len(all_amenities)))
|
||||||
if floor >= 13: # Add premium amenities for luxury suites
|
if floor >= 13:
|
||||||
premium_count = min(2, len(premium_amenities))
|
premium_count = min(2, len(premium_amenities))
|
||||||
selected_amenities.extend(random.sample(premium_amenities, premium_count))
|
selected_amenities.extend(random.sample(premium_amenities, premium_count))
|
||||||
|
|
||||||
# Room size
|
|
||||||
size_min, size_max = size_range
|
size_min, size_max = size_range
|
||||||
room_size = f"{random.randint(size_min, size_max)} sqm"
|
room_size = f'{random.randint(size_min, size_max)} sqm'
|
||||||
|
|
||||||
# Capacity (based on room type, with some variation)
|
|
||||||
capacity = room_type.capacity
|
capacity = room_type.capacity
|
||||||
if random.random() > 0.7: # 30% chance to have different capacity
|
if random.random() > 0.7:
|
||||||
capacity = max(1, capacity + random.randint(-1, 1))
|
capacity = max(1, capacity + random.randint(-1, 1))
|
||||||
|
room_number = f'{floor}{room_counter % 100:02d}'
|
||||||
# Room number
|
|
||||||
room_number = f"{floor}{room_counter % 100:02d}"
|
|
||||||
room_counter += 1
|
room_counter += 1
|
||||||
|
|
||||||
# Select 3 unique images for each room (ensure we always have images)
|
|
||||||
# Shuffle the list each time to get different combinations
|
|
||||||
shuffled_images = luxury_room_images.copy()
|
shuffled_images = luxury_room_images.copy()
|
||||||
random.shuffle(shuffled_images)
|
random.shuffle(shuffled_images)
|
||||||
image_urls = shuffled_images[:3] # Always take first 3 after shuffle
|
image_urls = shuffled_images[:3]
|
||||||
|
descriptions = [f'Elegantly designed {view_type.lower()} room with modern luxury amenities and breathtaking views.', f'Spacious {view_type.lower()} accommodation featuring premium furnishings and world-class comfort.', f'Luxurious {view_type.lower()} room with sophisticated decor and exceptional attention to detail.', f'Exquisite {view_type.lower()} suite offering unparalleled elegance and personalized service.', f'Opulent {view_type.lower()} accommodation with bespoke interiors and premium amenities.']
|
||||||
# Description
|
|
||||||
descriptions = [
|
|
||||||
f"Elegantly designed {view_type.lower()} room with modern luxury amenities and breathtaking views.",
|
|
||||||
f"Spacious {view_type.lower()} accommodation featuring premium furnishings and world-class comfort.",
|
|
||||||
f"Luxurious {view_type.lower()} room with sophisticated decor and exceptional attention to detail.",
|
|
||||||
f"Exquisite {view_type.lower()} suite offering unparalleled elegance and personalized service.",
|
|
||||||
f"Opulent {view_type.lower()} accommodation with bespoke interiors and premium amenities.",
|
|
||||||
]
|
|
||||||
description = random.choice(descriptions)
|
description = random.choice(descriptions)
|
||||||
|
status_weights = [0.85, 0.05, 0.05, 0.05]
|
||||||
# Status (mostly available, some in maintenance/cleaning)
|
status = random.choices([RoomStatus.available, RoomStatus.occupied, RoomStatus.maintenance, RoomStatus.cleaning], weights=status_weights)[0]
|
||||||
status_weights = [0.85, 0.05, 0.05, 0.05] # available, occupied, maintenance, cleaning
|
room = Room(room_type_id=room_type.id, room_number=room_number, floor=floor, status=status, price=price, featured=featured, capacity=capacity, room_size=room_size, view=view_type, images=json.dumps(image_urls), amenities=json.dumps(selected_amenities), description=description)
|
||||||
status = random.choices(
|
|
||||||
[RoomStatus.available, RoomStatus.occupied, RoomStatus.maintenance, RoomStatus.cleaning],
|
|
||||||
weights=status_weights
|
|
||||||
)[0]
|
|
||||||
|
|
||||||
# Create room
|
|
||||||
room = Room(
|
|
||||||
room_type_id=room_type.id,
|
|
||||||
room_number=room_number,
|
|
||||||
floor=floor,
|
|
||||||
status=status,
|
|
||||||
price=price,
|
|
||||||
featured=featured,
|
|
||||||
capacity=capacity,
|
|
||||||
room_size=room_size,
|
|
||||||
view=view_type,
|
|
||||||
images=json.dumps(image_urls),
|
|
||||||
amenities=json.dumps(selected_amenities),
|
|
||||||
description=description
|
|
||||||
)
|
|
||||||
|
|
||||||
db.add(room)
|
db.add(room)
|
||||||
rooms_created.append({
|
rooms_created.append({'number': room_number, 'floor': floor, 'type': room_type.name, 'view': view_type, 'price': price})
|
||||||
'number': room_number,
|
print(f' ✓ Created Room {room_number} - Floor {floor}, {room_type.name}, {view_type}, {room_size}, €{price:.2f}')
|
||||||
'floor': floor,
|
|
||||||
'type': room_type.name,
|
|
||||||
'view': view_type,
|
|
||||||
'price': price
|
|
||||||
})
|
|
||||||
|
|
||||||
print(f" ✓ Created Room {room_number} - Floor {floor}, {room_type.name}, {view_type}, {room_size}, €{price:.2f}")
|
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
print(f"\n✅ Successfully created {len(rooms_created)} luxury rooms!")
|
print(f'\n✅ Successfully created {len(rooms_created)} luxury rooms!')
|
||||||
print(f"\n📊 Summary:")
|
print(f'\n📊 Summary:')
|
||||||
featured_count = sum(1 for r in rooms_created if any(
|
featured_count = sum((1 for r in rooms_created if any((config[5] and r['floor'] >= config[0][0] and (r['floor'] <= config[0][1]) for config in room_configs))))
|
||||||
config[5] and r['floor'] >= config[0][0] and r['floor'] <= config[0][1]
|
print(f' - Featured rooms: {featured_count}')
|
||||||
for config in room_configs
|
print(f' - Floors: {min((r['floor'] for r in rooms_created))} - {max((r['floor'] for r in rooms_created))}')
|
||||||
))
|
print(f' - Price range: €{min((r['price'] for r in rooms_created)):.2f} - €{max((r['price'] for r in rooms_created)):.2f}')
|
||||||
print(f" - Featured rooms: {featured_count}")
|
print('=' * 80)
|
||||||
print(f" - Floors: {min(r['floor'] for r in rooms_created)} - {max(r['floor'] for r in rooms_created)}")
|
if __name__ == '__main__':
|
||||||
print(f" - Price range: €{min(r['price'] for r in rooms_created):.2f} - €{max(r['price'] for r in rooms_created):.2f}")
|
|
||||||
print("=" * 80)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
db = get_db()
|
db = get_db()
|
||||||
try:
|
try:
|
||||||
seed_rooms(db)
|
seed_rooms(db)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"\n❌ Error: {e}")
|
print(f'\n❌ Error: {e}')
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
db.rollback()
|
db.rollback()
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
# Hotel Booking Server Package
|
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -4,54 +4,25 @@ from sqlalchemy.orm import sessionmaker
|
|||||||
from sqlalchemy.pool import QueuePool
|
from sqlalchemy.pool import QueuePool
|
||||||
from .settings import settings
|
from .settings import settings
|
||||||
from .logging_config import get_logger
|
from .logging_config import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
# Database configuration using settings
|
|
||||||
DATABASE_URL = settings.database_url
|
DATABASE_URL = settings.database_url
|
||||||
|
engine = create_engine(DATABASE_URL, poolclass=QueuePool, pool_pre_ping=True, pool_recycle=3600, pool_size=10, max_overflow=20, echo=settings.is_development, future=True, connect_args={'charset': 'utf8mb4', 'connect_timeout': 10})
|
||||||
|
|
||||||
# Enhanced engine configuration for enterprise use
|
@event.listens_for(engine, 'connect')
|
||||||
engine = create_engine(
|
|
||||||
DATABASE_URL,
|
|
||||||
poolclass=QueuePool,
|
|
||||||
pool_pre_ping=True, # Verify connections before using
|
|
||||||
pool_recycle=3600, # Recycle connections after 1 hour
|
|
||||||
pool_size=10, # Number of connections to maintain
|
|
||||||
max_overflow=20, # Additional connections beyond pool_size
|
|
||||||
echo=settings.is_development, # Log SQL queries in development
|
|
||||||
future=True, # Use SQLAlchemy 2.0 style
|
|
||||||
connect_args={
|
|
||||||
"charset": "utf8mb4",
|
|
||||||
"connect_timeout": 10
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Event listeners for connection pool monitoring
|
|
||||||
@event.listens_for(engine, "connect")
|
|
||||||
def set_sqlite_pragma(dbapi_conn, connection_record):
|
def set_sqlite_pragma(dbapi_conn, connection_record):
|
||||||
"""Set connection-level settings"""
|
logger.debug('New database connection established')
|
||||||
logger.debug("New database connection established")
|
|
||||||
|
|
||||||
@event.listens_for(engine, "checkout")
|
@event.listens_for(engine, 'checkout')
|
||||||
def receive_checkout(dbapi_conn, connection_record, connection_proxy):
|
def receive_checkout(dbapi_conn, connection_record, connection_proxy):
|
||||||
"""Log connection checkout"""
|
logger.debug('Connection checked out from pool')
|
||||||
logger.debug("Connection checked out from pool")
|
|
||||||
|
|
||||||
@event.listens_for(engine, "checkin")
|
@event.listens_for(engine, 'checkin')
|
||||||
def receive_checkin(dbapi_conn, connection_record):
|
def receive_checkin(dbapi_conn, connection_record):
|
||||||
"""Log connection checkin"""
|
logger.debug('Connection returned to pool')
|
||||||
logger.debug("Connection returned to pool")
|
|
||||||
|
|
||||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
|
|
||||||
# Dependency to get DB session
|
|
||||||
def get_db():
|
def get_db():
|
||||||
"""
|
|
||||||
Dependency for getting database session.
|
|
||||||
Automatically handles session lifecycle.
|
|
||||||
"""
|
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
yield db
|
yield db
|
||||||
@@ -59,5 +30,4 @@ def get_db():
|
|||||||
db.rollback()
|
db.rollback()
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
"""
|
|
||||||
Enterprise-grade structured logging configuration
|
|
||||||
"""
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
@@ -8,89 +5,32 @@ from pathlib import Path
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
from .settings import settings
|
from .settings import settings
|
||||||
|
|
||||||
|
def setup_logging(log_level: Optional[str]=None, log_file: Optional[str]=None, enable_file_logging: bool=True) -> logging.Logger:
|
||||||
def setup_logging(
|
|
||||||
log_level: Optional[str] = None,
|
|
||||||
log_file: Optional[str] = None,
|
|
||||||
enable_file_logging: bool = True
|
|
||||||
) -> logging.Logger:
|
|
||||||
"""
|
|
||||||
Setup structured logging with file and console handlers
|
|
||||||
|
|
||||||
Args:
|
|
||||||
log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
||||||
log_file: Path to log file
|
|
||||||
enable_file_logging: Whether to enable file logging
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Configured root logger
|
|
||||||
"""
|
|
||||||
# Get configuration from settings
|
|
||||||
level = log_level or settings.LOG_LEVEL
|
level = log_level or settings.LOG_LEVEL
|
||||||
log_file_path = log_file or settings.LOG_FILE
|
log_file_path = log_file or settings.LOG_FILE
|
||||||
|
|
||||||
# Convert string level to logging constant
|
|
||||||
numeric_level = getattr(logging, level.upper(), logging.INFO)
|
numeric_level = getattr(logging, level.upper(), logging.INFO)
|
||||||
|
|
||||||
# Create logs directory if it doesn't exist
|
|
||||||
if enable_file_logging and log_file_path:
|
if enable_file_logging and log_file_path:
|
||||||
log_path = Path(log_file_path)
|
log_path = Path(log_file_path)
|
||||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
detailed_formatter = logging.Formatter(fmt='%(asctime)s | %(levelname)-8s | %(name)s | %(funcName)s:%(lineno)d | %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
|
||||||
# Create formatter with structured format
|
simple_formatter = logging.Formatter(fmt='%(asctime)s | %(levelname)-8s | %(message)s', datefmt='%H:%M:%S')
|
||||||
detailed_formatter = logging.Formatter(
|
|
||||||
fmt='%(asctime)s | %(levelname)-8s | %(name)s | %(funcName)s:%(lineno)d | %(message)s',
|
|
||||||
datefmt='%Y-%m-%d %H:%M:%S'
|
|
||||||
)
|
|
||||||
|
|
||||||
simple_formatter = logging.Formatter(
|
|
||||||
fmt='%(asctime)s | %(levelname)-8s | %(message)s',
|
|
||||||
datefmt='%H:%M:%S'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Configure root logger
|
|
||||||
root_logger = logging.getLogger()
|
root_logger = logging.getLogger()
|
||||||
root_logger.setLevel(numeric_level)
|
root_logger.setLevel(numeric_level)
|
||||||
|
|
||||||
# Remove existing handlers
|
|
||||||
root_logger.handlers.clear()
|
root_logger.handlers.clear()
|
||||||
|
|
||||||
# Console handler (always enabled)
|
|
||||||
console_handler = logging.StreamHandler(sys.stdout)
|
console_handler = logging.StreamHandler(sys.stdout)
|
||||||
console_handler.setLevel(numeric_level)
|
console_handler.setLevel(numeric_level)
|
||||||
console_handler.setFormatter(simple_formatter if settings.is_development else detailed_formatter)
|
console_handler.setFormatter(simple_formatter if settings.is_development else detailed_formatter)
|
||||||
root_logger.addHandler(console_handler)
|
root_logger.addHandler(console_handler)
|
||||||
|
if enable_file_logging and log_file_path and (not settings.is_development):
|
||||||
# File handler (rotating) - Disabled in development to avoid file watcher issues
|
file_handler = RotatingFileHandler(log_file_path, maxBytes=settings.LOG_MAX_BYTES, backupCount=settings.LOG_BACKUP_COUNT, encoding='utf-8')
|
||||||
if enable_file_logging and log_file_path and not settings.is_development:
|
|
||||||
file_handler = RotatingFileHandler(
|
|
||||||
log_file_path,
|
|
||||||
maxBytes=settings.LOG_MAX_BYTES,
|
|
||||||
backupCount=settings.LOG_BACKUP_COUNT,
|
|
||||||
encoding='utf-8'
|
|
||||||
)
|
|
||||||
file_handler.setLevel(numeric_level)
|
file_handler.setLevel(numeric_level)
|
||||||
file_handler.setFormatter(detailed_formatter)
|
file_handler.setFormatter(detailed_formatter)
|
||||||
root_logger.addHandler(file_handler)
|
root_logger.addHandler(file_handler)
|
||||||
|
logging.getLogger('uvicorn').setLevel(logging.INFO)
|
||||||
# Set levels for third-party loggers
|
logging.getLogger('uvicorn.access').setLevel(logging.INFO if settings.is_development else logging.WARNING)
|
||||||
logging.getLogger("uvicorn").setLevel(logging.INFO)
|
logging.getLogger('sqlalchemy.engine').setLevel(logging.WARNING)
|
||||||
logging.getLogger("uvicorn.access").setLevel(logging.INFO if settings.is_development else logging.WARNING)
|
logging.getLogger('slowapi').setLevel(logging.WARNING)
|
||||||
logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)
|
|
||||||
logging.getLogger("slowapi").setLevel(logging.WARNING)
|
|
||||||
|
|
||||||
return root_logger
|
return root_logger
|
||||||
|
|
||||||
|
|
||||||
def get_logger(name: str) -> logging.Logger:
|
def get_logger(name: str) -> logging.Logger:
|
||||||
"""
|
return logging.getLogger(name)
|
||||||
Get a logger instance with the given name
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: Logger name (typically __name__)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Logger instance
|
|
||||||
"""
|
|
||||||
return logging.getLogger(name)
|
|
||||||
|
|
||||||
@@ -1,129 +1,72 @@
|
|||||||
"""
|
|
||||||
Enterprise-grade configuration management using Pydantic Settings
|
|
||||||
"""
|
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
from typing import List
|
from typing import List
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
"""Application settings with environment variable support"""
|
model_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8', case_sensitive=False, extra='ignore')
|
||||||
|
APP_NAME: str = Field(default='Hotel Booking API', description='Application name')
|
||||||
model_config = SettingsConfigDict(
|
APP_VERSION: str = Field(default='1.0.0', description='Application version')
|
||||||
env_file=".env",
|
ENVIRONMENT: str = Field(default='development', description='Environment: development, staging, production')
|
||||||
env_file_encoding="utf-8",
|
DEBUG: bool = Field(default=False, description='Debug mode')
|
||||||
case_sensitive=False,
|
API_V1_PREFIX: str = Field(default='/api/v1', description='API v1 prefix')
|
||||||
extra="ignore"
|
HOST: str = Field(default='0.0.0.0', description='Server host')
|
||||||
)
|
PORT: int = Field(default=8000, description='Server port')
|
||||||
|
DB_USER: str = Field(default='root', description='Database user')
|
||||||
# Application
|
DB_PASS: str = Field(default='', description='Database password')
|
||||||
APP_NAME: str = Field(default="Hotel Booking API", description="Application name")
|
DB_NAME: str = Field(default='hotel_db', description='Database name')
|
||||||
APP_VERSION: str = Field(default="1.0.0", description="Application version")
|
DB_HOST: str = Field(default='localhost', description='Database host')
|
||||||
ENVIRONMENT: str = Field(default="development", description="Environment: development, staging, production")
|
DB_PORT: str = Field(default='3306', description='Database port')
|
||||||
DEBUG: bool = Field(default=False, description="Debug mode")
|
JWT_SECRET: str = Field(default='dev-secret-key-change-in-production-12345', description='JWT secret key')
|
||||||
API_V1_PREFIX: str = Field(default="/api/v1", description="API v1 prefix")
|
JWT_ALGORITHM: str = Field(default='HS256', description='JWT algorithm')
|
||||||
|
JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = Field(default=30, description='JWT access token expiration in minutes')
|
||||||
# Server
|
JWT_REFRESH_TOKEN_EXPIRE_DAYS: int = Field(default=7, description='JWT refresh token expiration in days')
|
||||||
HOST: str = Field(default="0.0.0.0", description="Server host")
|
CLIENT_URL: str = Field(default='http://localhost:5173', description='Frontend client URL')
|
||||||
PORT: int = Field(default=8000, description="Server port")
|
CORS_ORIGINS: List[str] = Field(default_factory=lambda: ['http://localhost:5173', 'http://localhost:3000', 'http://127.0.0.1:5173'], description='Allowed CORS origins')
|
||||||
|
RATE_LIMIT_ENABLED: bool = Field(default=True, description='Enable rate limiting')
|
||||||
# Database
|
RATE_LIMIT_PER_MINUTE: int = Field(default=60, description='Requests per minute per IP')
|
||||||
DB_USER: str = Field(default="root", description="Database user")
|
LOG_LEVEL: str = Field(default='INFO', description='Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL')
|
||||||
DB_PASS: str = Field(default="", description="Database password")
|
LOG_FILE: str = Field(default='logs/app.log', description='Log file path')
|
||||||
DB_NAME: str = Field(default="hotel_db", description="Database name")
|
LOG_MAX_BYTES: int = Field(default=10485760, description='Max log file size (10MB)')
|
||||||
DB_HOST: str = Field(default="localhost", description="Database host")
|
LOG_BACKUP_COUNT: int = Field(default=5, description='Number of backup log files')
|
||||||
DB_PORT: str = Field(default="3306", description="Database port")
|
SMTP_HOST: str = Field(default='smtp.gmail.com', description='SMTP host')
|
||||||
|
SMTP_PORT: int = Field(default=587, description='SMTP port')
|
||||||
# Security
|
SMTP_USER: str = Field(default='', description='SMTP username')
|
||||||
JWT_SECRET: str = Field(default="dev-secret-key-change-in-production-12345", description="JWT secret key")
|
SMTP_PASSWORD: str = Field(default='', description='SMTP password')
|
||||||
JWT_ALGORITHM: str = Field(default="HS256", description="JWT algorithm")
|
SMTP_FROM_EMAIL: str = Field(default='', description='From email address')
|
||||||
JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = Field(default=30, description="JWT access token expiration in minutes")
|
SMTP_FROM_NAME: str = Field(default='Hotel Booking', description='From name')
|
||||||
JWT_REFRESH_TOKEN_EXPIRE_DAYS: int = Field(default=7, description="JWT refresh token expiration in days")
|
UPLOAD_DIR: str = Field(default='uploads', description='Upload directory')
|
||||||
|
MAX_UPLOAD_SIZE: int = Field(default=5242880, description='Max upload size in bytes (5MB)')
|
||||||
# CORS
|
ALLOWED_EXTENSIONS: List[str] = Field(default_factory=lambda: ['jpg', 'jpeg', 'png', 'gif', 'webp'], description='Allowed file extensions')
|
||||||
CLIENT_URL: str = Field(default="http://localhost:5173", description="Frontend client URL")
|
REDIS_ENABLED: bool = Field(default=False, description='Enable Redis caching')
|
||||||
CORS_ORIGINS: List[str] = Field(
|
REDIS_HOST: str = Field(default='localhost', description='Redis host')
|
||||||
default_factory=lambda: [
|
REDIS_PORT: int = Field(default=6379, description='Redis port')
|
||||||
"http://localhost:5173",
|
REDIS_DB: int = Field(default=0, description='Redis database number')
|
||||||
"http://localhost:3000",
|
REDIS_PASSWORD: str = Field(default='', description='Redis password')
|
||||||
"http://127.0.0.1:5173"
|
REQUEST_TIMEOUT: int = Field(default=30, description='Request timeout in seconds')
|
||||||
],
|
HEALTH_CHECK_INTERVAL: int = Field(default=30, description='Health check interval in seconds')
|
||||||
description="Allowed CORS origins"
|
STRIPE_SECRET_KEY: str = Field(default='', description='Stripe secret key')
|
||||||
)
|
STRIPE_PUBLISHABLE_KEY: str = Field(default='', description='Stripe publishable key')
|
||||||
|
STRIPE_WEBHOOK_SECRET: str = Field(default='', description='Stripe webhook secret')
|
||||||
# Rate Limiting
|
PAYPAL_CLIENT_ID: str = Field(default='', description='PayPal client ID')
|
||||||
RATE_LIMIT_ENABLED: bool = Field(default=True, description="Enable rate limiting")
|
PAYPAL_CLIENT_SECRET: str = Field(default='', description='PayPal client secret')
|
||||||
RATE_LIMIT_PER_MINUTE: int = Field(default=60, description="Requests per minute per IP")
|
PAYPAL_MODE: str = Field(default='sandbox', description='PayPal mode: sandbox or live')
|
||||||
|
|
||||||
# Logging
|
|
||||||
LOG_LEVEL: str = Field(default="INFO", description="Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL")
|
|
||||||
LOG_FILE: str = Field(default="logs/app.log", description="Log file path")
|
|
||||||
LOG_MAX_BYTES: int = Field(default=10485760, description="Max log file size (10MB)")
|
|
||||||
LOG_BACKUP_COUNT: int = Field(default=5, description="Number of backup log files")
|
|
||||||
|
|
||||||
# Email
|
|
||||||
SMTP_HOST: str = Field(default="smtp.gmail.com", description="SMTP host")
|
|
||||||
SMTP_PORT: int = Field(default=587, description="SMTP port")
|
|
||||||
SMTP_USER: str = Field(default="", description="SMTP username")
|
|
||||||
SMTP_PASSWORD: str = Field(default="", description="SMTP password")
|
|
||||||
SMTP_FROM_EMAIL: str = Field(default="", description="From email address")
|
|
||||||
SMTP_FROM_NAME: str = Field(default="Hotel Booking", description="From name")
|
|
||||||
|
|
||||||
# File Upload
|
|
||||||
UPLOAD_DIR: str = Field(default="uploads", description="Upload directory")
|
|
||||||
MAX_UPLOAD_SIZE: int = Field(default=5242880, description="Max upload size in bytes (5MB)")
|
|
||||||
ALLOWED_EXTENSIONS: List[str] = Field(
|
|
||||||
default_factory=lambda: ["jpg", "jpeg", "png", "gif", "webp"],
|
|
||||||
description="Allowed file extensions"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Redis (for caching)
|
|
||||||
REDIS_ENABLED: bool = Field(default=False, description="Enable Redis caching")
|
|
||||||
REDIS_HOST: str = Field(default="localhost", description="Redis host")
|
|
||||||
REDIS_PORT: int = Field(default=6379, description="Redis port")
|
|
||||||
REDIS_DB: int = Field(default=0, description="Redis database number")
|
|
||||||
REDIS_PASSWORD: str = Field(default="", description="Redis password")
|
|
||||||
|
|
||||||
# Request Timeout
|
|
||||||
REQUEST_TIMEOUT: int = Field(default=30, description="Request timeout in seconds")
|
|
||||||
|
|
||||||
# Health Check
|
|
||||||
HEALTH_CHECK_INTERVAL: int = Field(default=30, description="Health check interval in seconds")
|
|
||||||
|
|
||||||
# Stripe Payment Gateway
|
|
||||||
STRIPE_SECRET_KEY: str = Field(default="", description="Stripe secret key")
|
|
||||||
STRIPE_PUBLISHABLE_KEY: str = Field(default="", description="Stripe publishable key")
|
|
||||||
STRIPE_WEBHOOK_SECRET: str = Field(default="", description="Stripe webhook secret")
|
|
||||||
|
|
||||||
# PayPal Payment Gateway
|
|
||||||
PAYPAL_CLIENT_ID: str = Field(default="", description="PayPal client ID")
|
|
||||||
PAYPAL_CLIENT_SECRET: str = Field(default="", description="PayPal client secret")
|
|
||||||
PAYPAL_MODE: str = Field(default="sandbox", description="PayPal mode: sandbox or live")
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def database_url(self) -> str:
|
def database_url(self) -> str:
|
||||||
"""Construct database URL"""
|
return f'mysql+pymysql://{self.DB_USER}:{self.DB_PASS}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}'
|
||||||
return f"mysql+pymysql://{self.DB_USER}:{self.DB_PASS}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}"
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_production(self) -> bool:
|
def is_production(self) -> bool:
|
||||||
"""Check if running in production"""
|
return self.ENVIRONMENT.lower() == 'production'
|
||||||
return self.ENVIRONMENT.lower() == "production"
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_development(self) -> bool:
|
def is_development(self) -> bool:
|
||||||
"""Check if running in development"""
|
return self.ENVIRONMENT.lower() == 'development'
|
||||||
return self.ENVIRONMENT.lower() == "development"
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def redis_url(self) -> str:
|
def redis_url(self) -> str:
|
||||||
"""Construct Redis URL"""
|
|
||||||
if self.REDIS_PASSWORD:
|
if self.REDIS_PASSWORD:
|
||||||
return f"redis://:{self.REDIS_PASSWORD}@{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}"
|
return f'redis://:{self.REDIS_PASSWORD}@{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}'
|
||||||
return f"redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}"
|
return f'redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}'
|
||||||
|
settings = Settings()
|
||||||
|
|
||||||
# Global settings instance
|
|
||||||
settings = Settings()
|
|
||||||
|
|
||||||
@@ -11,222 +11,111 @@ from slowapi.errors import RateLimitExceeded
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
# Import configuration and logging FIRST
|
|
||||||
from .config.settings import settings
|
from .config.settings import settings
|
||||||
from .config.logging_config import setup_logging, get_logger
|
from .config.logging_config import setup_logging, get_logger
|
||||||
from .config.database import engine, Base, get_db
|
from .config.database import engine, Base, get_db
|
||||||
from . import models # noqa: F401 - ensure models are imported so tables are created
|
from . import models
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
# Setup logging before anything else
|
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
|
logger.info(f'Starting {settings.APP_NAME} v{settings.APP_VERSION} in {settings.ENVIRONMENT} mode')
|
||||||
logger.info(f"Starting {settings.APP_NAME} v{settings.APP_VERSION} in {settings.ENVIRONMENT} mode")
|
from .middleware.error_handler import validation_exception_handler, integrity_error_handler, jwt_error_handler, http_exception_handler, general_exception_handler
|
||||||
|
|
||||||
# Import middleware
|
|
||||||
from .middleware.error_handler import (
|
|
||||||
validation_exception_handler,
|
|
||||||
integrity_error_handler,
|
|
||||||
jwt_error_handler,
|
|
||||||
http_exception_handler,
|
|
||||||
general_exception_handler
|
|
||||||
)
|
|
||||||
from .middleware.request_id import RequestIDMiddleware
|
from .middleware.request_id import RequestIDMiddleware
|
||||||
from .middleware.security import SecurityHeadersMiddleware
|
from .middleware.security import SecurityHeadersMiddleware
|
||||||
from .middleware.timeout import TimeoutMiddleware
|
from .middleware.timeout import TimeoutMiddleware
|
||||||
from .middleware.cookie_consent import CookieConsentMiddleware
|
from .middleware.cookie_consent import CookieConsentMiddleware
|
||||||
|
|
||||||
# Create database tables (for development, migrations should be used in production)
|
|
||||||
if settings.is_development:
|
if settings.is_development:
|
||||||
logger.info("Creating database tables (development mode)")
|
logger.info('Creating database tables (development mode)')
|
||||||
Base.metadata.create_all(bind=engine)
|
Base.metadata.create_all(bind=engine)
|
||||||
else:
|
else:
|
||||||
# Ensure new tables exist even if full migrations haven't been run yet.
|
|
||||||
try:
|
try:
|
||||||
from .models.cookie_policy import CookiePolicy
|
from .models.cookie_policy import CookiePolicy
|
||||||
from .models.cookie_integration_config import CookieIntegrationConfig
|
from .models.cookie_integration_config import CookieIntegrationConfig
|
||||||
from .models.page_content import PageContent
|
from .models.page_content import PageContent
|
||||||
logger.info("Ensuring required tables exist")
|
logger.info('Ensuring required tables exist')
|
||||||
CookiePolicy.__table__.create(bind=engine, checkfirst=True)
|
CookiePolicy.__table__.create(bind=engine, checkfirst=True)
|
||||||
CookieIntegrationConfig.__table__.create(bind=engine, checkfirst=True)
|
CookieIntegrationConfig.__table__.create(bind=engine, checkfirst=True)
|
||||||
PageContent.__table__.create(bind=engine, checkfirst=True)
|
PageContent.__table__.create(bind=engine, checkfirst=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to ensure required tables exist: {e}")
|
logger.error(f'Failed to ensure required tables exist: {e}')
|
||||||
|
|
||||||
from .routes import auth_routes
|
from .routes import auth_routes
|
||||||
from .routes import privacy_routes
|
from .routes import privacy_routes
|
||||||
|
app = FastAPI(title=settings.APP_NAME, description='Enterprise-grade Hotel Booking API', version=settings.APP_VERSION, docs_url='/api/docs' if not settings.is_production else None, redoc_url='/api/redoc' if not settings.is_production else None, openapi_url='/api/openapi.json' if not settings.is_production else None)
|
||||||
# Initialize FastAPI app
|
|
||||||
app = FastAPI(
|
|
||||||
title=settings.APP_NAME,
|
|
||||||
description="Enterprise-grade Hotel Booking API",
|
|
||||||
version=settings.APP_VERSION,
|
|
||||||
docs_url="/api/docs" if not settings.is_production else None,
|
|
||||||
redoc_url="/api/redoc" if not settings.is_production else None,
|
|
||||||
openapi_url="/api/openapi.json" if not settings.is_production else None
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add middleware in order (order matters!)
|
|
||||||
# 1. Request ID middleware (first to add request ID)
|
|
||||||
app.add_middleware(RequestIDMiddleware)
|
app.add_middleware(RequestIDMiddleware)
|
||||||
|
|
||||||
# 2. Cookie consent middleware (makes consent available on request.state)
|
|
||||||
app.add_middleware(CookieConsentMiddleware)
|
app.add_middleware(CookieConsentMiddleware)
|
||||||
|
|
||||||
# 3. Timeout middleware
|
|
||||||
if settings.REQUEST_TIMEOUT > 0:
|
if settings.REQUEST_TIMEOUT > 0:
|
||||||
app.add_middleware(TimeoutMiddleware)
|
app.add_middleware(TimeoutMiddleware)
|
||||||
|
|
||||||
# 4. Security headers middleware
|
|
||||||
app.add_middleware(SecurityHeadersMiddleware)
|
app.add_middleware(SecurityHeadersMiddleware)
|
||||||
|
|
||||||
# Rate limiting
|
|
||||||
if settings.RATE_LIMIT_ENABLED:
|
if settings.RATE_LIMIT_ENABLED:
|
||||||
limiter = Limiter(
|
limiter = Limiter(key_func=get_remote_address, default_limits=[f'{settings.RATE_LIMIT_PER_MINUTE}/minute'])
|
||||||
key_func=get_remote_address,
|
|
||||||
default_limits=[f"{settings.RATE_LIMIT_PER_MINUTE}/minute"]
|
|
||||||
)
|
|
||||||
app.state.limiter = limiter
|
app.state.limiter = limiter
|
||||||
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
||||||
logger.info(f"Rate limiting enabled: {settings.RATE_LIMIT_PER_MINUTE} requests/minute")
|
logger.info(f'Rate limiting enabled: {settings.RATE_LIMIT_PER_MINUTE} requests/minute')
|
||||||
|
|
||||||
# CORS configuration
|
|
||||||
if settings.is_development:
|
if settings.is_development:
|
||||||
# For development, use regex to allow any localhost port
|
app.add_middleware(CORSMiddleware, allow_origin_regex='http://(localhost|127\\.0\\.0\\.1)(:\\d+)?', allow_credentials=True, allow_methods=['*'], allow_headers=['*'])
|
||||||
app.add_middleware(
|
logger.info('CORS configured for development (allowing localhost)')
|
||||||
CORSMiddleware,
|
|
||||||
allow_origin_regex=r"http://(localhost|127\.0\.0\.1)(:\d+)?",
|
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
logger.info("CORS configured for development (allowing localhost)")
|
|
||||||
else:
|
else:
|
||||||
# Production: use specific origins
|
app.add_middleware(CORSMiddleware, allow_origins=settings.CORS_ORIGINS, allow_credentials=True, allow_methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], allow_headers=['*'])
|
||||||
app.add_middleware(
|
logger.info(f'CORS configured for production with {len(settings.CORS_ORIGINS)} allowed origins')
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=settings.CORS_ORIGINS,
|
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
logger.info(f"CORS configured for production with {len(settings.CORS_ORIGINS)} allowed origins")
|
|
||||||
|
|
||||||
# Serve static files (uploads)
|
|
||||||
uploads_dir = Path(__file__).parent.parent / settings.UPLOAD_DIR
|
uploads_dir = Path(__file__).parent.parent / settings.UPLOAD_DIR
|
||||||
uploads_dir.mkdir(exist_ok=True)
|
uploads_dir.mkdir(exist_ok=True)
|
||||||
app.mount("/uploads", StaticFiles(directory=str(uploads_dir)), name="uploads")
|
app.mount('/uploads', StaticFiles(directory=str(uploads_dir)), name='uploads')
|
||||||
|
|
||||||
# Exception handlers
|
|
||||||
app.add_exception_handler(HTTPException, http_exception_handler)
|
app.add_exception_handler(HTTPException, http_exception_handler)
|
||||||
app.add_exception_handler(RequestValidationError, validation_exception_handler)
|
app.add_exception_handler(RequestValidationError, validation_exception_handler)
|
||||||
app.add_exception_handler(IntegrityError, integrity_error_handler)
|
app.add_exception_handler(IntegrityError, integrity_error_handler)
|
||||||
app.add_exception_handler(JWTError, jwt_error_handler)
|
app.add_exception_handler(JWTError, jwt_error_handler)
|
||||||
app.add_exception_handler(Exception, general_exception_handler)
|
app.add_exception_handler(Exception, general_exception_handler)
|
||||||
|
|
||||||
# Enhanced Health check with database connectivity
|
@app.get('/health', tags=['health'])
|
||||||
@app.get("/health", tags=["health"])
|
@app.get('/api/health', tags=['health'])
|
||||||
@app.get("/api/health", tags=["health"])
|
async def health_check(db: Session=Depends(get_db)):
|
||||||
async def health_check(db: Session = Depends(get_db)):
|
health_status = {'status': 'healthy', 'timestamp': datetime.utcnow().isoformat(), 'service': settings.APP_NAME, 'version': settings.APP_VERSION, 'environment': settings.ENVIRONMENT, 'checks': {'api': 'ok', 'database': 'unknown'}}
|
||||||
"""
|
|
||||||
Enhanced health check endpoint with database connectivity test
|
|
||||||
Available at both /health and /api/health for consistency
|
|
||||||
"""
|
|
||||||
health_status = {
|
|
||||||
"status": "healthy",
|
|
||||||
"timestamp": datetime.utcnow().isoformat(),
|
|
||||||
"service": settings.APP_NAME,
|
|
||||||
"version": settings.APP_VERSION,
|
|
||||||
"environment": settings.ENVIRONMENT,
|
|
||||||
"checks": {
|
|
||||||
"api": "ok",
|
|
||||||
"database": "unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check database connectivity
|
|
||||||
try:
|
try:
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
db.execute(text("SELECT 1"))
|
db.execute(text('SELECT 1'))
|
||||||
health_status["checks"]["database"] = "ok"
|
health_status['checks']['database'] = 'ok'
|
||||||
except OperationalError as e:
|
except OperationalError as e:
|
||||||
health_status["status"] = "unhealthy"
|
health_status['status'] = 'unhealthy'
|
||||||
health_status["checks"]["database"] = "error"
|
health_status['checks']['database'] = 'error'
|
||||||
health_status["error"] = str(e)
|
health_status['error'] = str(e)
|
||||||
logger.error(f"Database health check failed: {str(e)}")
|
logger.error(f'Database health check failed: {str(e)}')
|
||||||
return JSONResponse(
|
return JSONResponse(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, content=health_status)
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
||||||
content=health_status
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
health_status["status"] = "unhealthy"
|
health_status['status'] = 'unhealthy'
|
||||||
health_status["checks"]["database"] = "error"
|
health_status['checks']['database'] = 'error'
|
||||||
health_status["error"] = str(e)
|
health_status['error'] = str(e)
|
||||||
logger.error(f"Health check failed: {str(e)}")
|
logger.error(f'Health check failed: {str(e)}')
|
||||||
return JSONResponse(
|
return JSONResponse(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, content=health_status)
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
||||||
content=health_status
|
|
||||||
)
|
|
||||||
|
|
||||||
return health_status
|
return health_status
|
||||||
|
|
||||||
|
@app.get('/metrics', tags=['monitoring'])
|
||||||
# Metrics endpoint (basic)
|
|
||||||
@app.get("/metrics", tags=["monitoring"])
|
|
||||||
async def metrics():
|
async def metrics():
|
||||||
"""
|
return {'status': 'success', 'service': settings.APP_NAME, 'version': settings.APP_VERSION, 'environment': settings.ENVIRONMENT, 'timestamp': datetime.utcnow().isoformat()}
|
||||||
Basic metrics endpoint (can be extended with Prometheus or similar)
|
app.include_router(auth_routes.router, prefix='/api')
|
||||||
"""
|
app.include_router(privacy_routes.router, prefix='/api')
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"service": settings.APP_NAME,
|
|
||||||
"version": settings.APP_VERSION,
|
|
||||||
"environment": settings.ENVIRONMENT,
|
|
||||||
"timestamp": datetime.utcnow().isoformat()
|
|
||||||
}
|
|
||||||
|
|
||||||
# API Routes with versioning
|
|
||||||
# Legacy routes (maintain backward compatibility)
|
|
||||||
app.include_router(auth_routes.router, prefix="/api")
|
|
||||||
app.include_router(privacy_routes.router, prefix="/api")
|
|
||||||
|
|
||||||
# Versioned API routes (v1)
|
|
||||||
app.include_router(auth_routes.router, prefix=settings.API_V1_PREFIX)
|
app.include_router(auth_routes.router, prefix=settings.API_V1_PREFIX)
|
||||||
app.include_router(privacy_routes.router, prefix=settings.API_V1_PREFIX)
|
app.include_router(privacy_routes.router, prefix=settings.API_V1_PREFIX)
|
||||||
|
from .routes import room_routes, booking_routes, payment_routes, invoice_routes, banner_routes, favorite_routes, service_routes, service_booking_routes, promotion_routes, report_routes, review_routes, user_routes, audit_routes, admin_privacy_routes, system_settings_routes, contact_routes, page_content_routes, home_routes, about_routes, contact_content_routes, footer_routes, chat_routes
|
||||||
# Import and include other routes
|
app.include_router(room_routes.router, prefix='/api')
|
||||||
from .routes import (
|
app.include_router(booking_routes.router, prefix='/api')
|
||||||
room_routes, booking_routes, payment_routes, invoice_routes, banner_routes,
|
app.include_router(payment_routes.router, prefix='/api')
|
||||||
favorite_routes, service_routes, service_booking_routes, promotion_routes, report_routes,
|
app.include_router(invoice_routes.router, prefix='/api')
|
||||||
review_routes, user_routes, audit_routes, admin_privacy_routes,
|
app.include_router(banner_routes.router, prefix='/api')
|
||||||
system_settings_routes, contact_routes, page_content_routes,
|
app.include_router(favorite_routes.router, prefix='/api')
|
||||||
home_routes, about_routes, contact_content_routes, footer_routes
|
app.include_router(service_routes.router, prefix='/api')
|
||||||
)
|
app.include_router(service_booking_routes.router, prefix='/api')
|
||||||
|
app.include_router(promotion_routes.router, prefix='/api')
|
||||||
# Legacy routes (maintain backward compatibility)
|
app.include_router(report_routes.router, prefix='/api')
|
||||||
app.include_router(room_routes.router, prefix="/api")
|
app.include_router(review_routes.router, prefix='/api')
|
||||||
app.include_router(booking_routes.router, prefix="/api")
|
app.include_router(user_routes.router, prefix='/api')
|
||||||
app.include_router(payment_routes.router, prefix="/api")
|
app.include_router(audit_routes.router, prefix='/api')
|
||||||
app.include_router(invoice_routes.router, prefix="/api")
|
app.include_router(admin_privacy_routes.router, prefix='/api')
|
||||||
app.include_router(banner_routes.router, prefix="/api")
|
app.include_router(system_settings_routes.router, prefix='/api')
|
||||||
app.include_router(favorite_routes.router, prefix="/api")
|
app.include_router(contact_routes.router, prefix='/api')
|
||||||
app.include_router(service_routes.router, prefix="/api")
|
app.include_router(home_routes.router, prefix='/api')
|
||||||
app.include_router(service_booking_routes.router, prefix="/api")
|
app.include_router(about_routes.router, prefix='/api')
|
||||||
app.include_router(promotion_routes.router, prefix="/api")
|
app.include_router(contact_content_routes.router, prefix='/api')
|
||||||
app.include_router(report_routes.router, prefix="/api")
|
app.include_router(footer_routes.router, prefix='/api')
|
||||||
app.include_router(review_routes.router, prefix="/api")
|
app.include_router(chat_routes.router, prefix='/api')
|
||||||
app.include_router(user_routes.router, prefix="/api")
|
|
||||||
app.include_router(audit_routes.router, prefix="/api")
|
|
||||||
app.include_router(admin_privacy_routes.router, prefix="/api")
|
|
||||||
app.include_router(system_settings_routes.router, prefix="/api")
|
|
||||||
app.include_router(contact_routes.router, prefix="/api")
|
|
||||||
app.include_router(home_routes.router, prefix="/api")
|
|
||||||
app.include_router(about_routes.router, prefix="/api")
|
|
||||||
app.include_router(contact_content_routes.router, prefix="/api")
|
|
||||||
app.include_router(footer_routes.router, prefix="/api")
|
|
||||||
|
|
||||||
# Versioned routes (v1)
|
|
||||||
app.include_router(room_routes.router, prefix=settings.API_V1_PREFIX)
|
app.include_router(room_routes.router, prefix=settings.API_V1_PREFIX)
|
||||||
app.include_router(booking_routes.router, prefix=settings.API_V1_PREFIX)
|
app.include_router(booking_routes.router, prefix=settings.API_V1_PREFIX)
|
||||||
app.include_router(payment_routes.router, prefix=settings.API_V1_PREFIX)
|
app.include_router(payment_routes.router, prefix=settings.API_V1_PREFIX)
|
||||||
@@ -247,52 +136,24 @@ app.include_router(home_routes.router, prefix=settings.API_V1_PREFIX)
|
|||||||
app.include_router(about_routes.router, prefix=settings.API_V1_PREFIX)
|
app.include_router(about_routes.router, prefix=settings.API_V1_PREFIX)
|
||||||
app.include_router(contact_content_routes.router, prefix=settings.API_V1_PREFIX)
|
app.include_router(contact_content_routes.router, prefix=settings.API_V1_PREFIX)
|
||||||
app.include_router(footer_routes.router, prefix=settings.API_V1_PREFIX)
|
app.include_router(footer_routes.router, prefix=settings.API_V1_PREFIX)
|
||||||
app.include_router(page_content_routes.router, prefix="/api")
|
app.include_router(chat_routes.router, prefix=settings.API_V1_PREFIX)
|
||||||
|
app.include_router(page_content_routes.router, prefix='/api')
|
||||||
app.include_router(page_content_routes.router, prefix=settings.API_V1_PREFIX)
|
app.include_router(page_content_routes.router, prefix=settings.API_V1_PREFIX)
|
||||||
|
logger.info('All routes registered successfully')
|
||||||
|
|
||||||
logger.info("All routes registered successfully")
|
@app.on_event('startup')
|
||||||
|
|
||||||
# Startup event
|
|
||||||
@app.on_event("startup")
|
|
||||||
async def startup_event():
|
async def startup_event():
|
||||||
"""Run on application startup"""
|
logger.info(f'{settings.APP_NAME} started successfully')
|
||||||
logger.info(f"{settings.APP_NAME} started successfully")
|
logger.info(f'Environment: {settings.ENVIRONMENT}')
|
||||||
logger.info(f"Environment: {settings.ENVIRONMENT}")
|
logger.info(f'Debug mode: {settings.DEBUG}')
|
||||||
logger.info(f"Debug mode: {settings.DEBUG}")
|
logger.info(f'API version: {settings.API_V1_PREFIX}')
|
||||||
logger.info(f"API version: {settings.API_V1_PREFIX}")
|
|
||||||
|
|
||||||
# Shutdown event
|
@app.on_event('shutdown')
|
||||||
@app.on_event("shutdown")
|
|
||||||
async def shutdown_event():
|
async def shutdown_event():
|
||||||
"""Run on application shutdown"""
|
logger.info(f'{settings.APP_NAME} shutting down gracefully')
|
||||||
logger.info(f"{settings.APP_NAME} shutting down gracefully")
|
if __name__ == '__main__':
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# Only watch the src directory to avoid watching logs, uploads, etc.
|
|
||||||
base_dir = Path(__file__).parent.parent
|
base_dir = Path(__file__).parent.parent
|
||||||
src_dir = str(base_dir / "src")
|
src_dir = str(base_dir / 'src')
|
||||||
|
uvicorn.run('src.main:app', host=settings.HOST, port=settings.PORT, reload=settings.is_development, log_level=settings.LOG_LEVEL.lower(), reload_dirs=[src_dir] if settings.is_development else None, reload_excludes=['*.log', '*.pyc', '*.pyo', '*.pyd', '__pycache__', '**/__pycache__/**', '*.db', '*.sqlite', '*.sqlite3'], reload_delay=0.5)
|
||||||
uvicorn.run(
|
|
||||||
"src.main:app",
|
|
||||||
host=settings.HOST,
|
|
||||||
port=settings.PORT,
|
|
||||||
reload=settings.is_development,
|
|
||||||
log_level=settings.LOG_LEVEL.lower(),
|
|
||||||
reload_dirs=[src_dir] if settings.is_development else None,
|
|
||||||
reload_excludes=[
|
|
||||||
"*.log",
|
|
||||||
"*.pyc",
|
|
||||||
"*.pyo",
|
|
||||||
"*.pyd",
|
|
||||||
"__pycache__",
|
|
||||||
"**/__pycache__/**",
|
|
||||||
"*.db",
|
|
||||||
"*.sqlite",
|
|
||||||
"*.sqlite3"
|
|
||||||
],
|
|
||||||
reload_delay=0.5 # Increase delay to reduce false positives
|
|
||||||
)
|
|
||||||
|
|
||||||
Binary file not shown.
@@ -4,71 +4,56 @@ from jose import JWTError, jwt
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from ..config.database import get_db
|
from ..config.database import get_db
|
||||||
from ..config.settings import settings
|
from ..config.settings import settings
|
||||||
from ..models.user import User
|
from ..models.user import User
|
||||||
from ..models.role import Role
|
from ..models.role import Role
|
||||||
|
|
||||||
security = HTTPBearer()
|
security = HTTPBearer()
|
||||||
|
|
||||||
|
def get_current_user(credentials: HTTPAuthorizationCredentials=Depends(security), db: Session=Depends(get_db)) -> User:
|
||||||
def get_current_user(
|
|
||||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
) -> User:
|
|
||||||
"""
|
|
||||||
Verify JWT token and return current user
|
|
||||||
"""
|
|
||||||
token = credentials.credentials
|
token = credentials.credentials
|
||||||
credentials_exception = HTTPException(
|
credentials_exception = HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Could not validate credentials', headers={'WWW-Authenticate': 'Bearer'})
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
||||||
detail="Could not validate credentials",
|
|
||||||
headers={"WWW-Authenticate": "Bearer"},
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
jwt_secret = getattr(settings, 'JWT_SECRET', None) or os.getenv("JWT_SECRET", "dev-secret-key-change-in-production-12345")
|
jwt_secret = getattr(settings, 'JWT_SECRET', None) or os.getenv('JWT_SECRET', 'dev-secret-key-change-in-production-12345')
|
||||||
payload = jwt.decode(token, jwt_secret, algorithms=["HS256"])
|
payload = jwt.decode(token, jwt_secret, algorithms=['HS256'])
|
||||||
user_id: int = payload.get("userId")
|
user_id: int = payload.get('userId')
|
||||||
if user_id is None:
|
if user_id is None:
|
||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
except JWTError:
|
except JWTError:
|
||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
|
|
||||||
user = db.query(User).filter(User.id == user_id).first()
|
user = db.query(User).filter(User.id == user_id).first()
|
||||||
if user is None:
|
if user is None:
|
||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
def authorize_roles(*allowed_roles: str):
|
def authorize_roles(*allowed_roles: str):
|
||||||
"""
|
|
||||||
Check if user has required role
|
def role_checker(current_user: User=Depends(get_current_user), db: Session=Depends(get_db)) -> User:
|
||||||
"""
|
|
||||||
def role_checker(
|
|
||||||
current_user: User = Depends(get_current_user),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
) -> User:
|
|
||||||
# Query the role from database instead of using hardcoded IDs
|
|
||||||
role = db.query(Role).filter(Role.id == current_user.role_id).first()
|
role = db.query(Role).filter(Role.id == current_user.role_id).first()
|
||||||
|
|
||||||
if not role:
|
if not role:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='User role not found')
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
|
||||||
detail="User role not found"
|
|
||||||
)
|
|
||||||
|
|
||||||
user_role_name = role.name
|
user_role_name = role.name
|
||||||
|
|
||||||
if user_role_name not in allowed_roles:
|
if user_role_name not in allowed_roles:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='You do not have permission to access this resource')
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
|
||||||
detail="You do not have permission to access this resource"
|
|
||||||
)
|
|
||||||
|
|
||||||
return current_user
|
return current_user
|
||||||
|
|
||||||
return role_checker
|
return role_checker
|
||||||
|
|
||||||
|
def get_current_user_optional(credentials: Optional[HTTPAuthorizationCredentials]=Depends(HTTPBearer(auto_error=False)), db: Session=Depends(get_db)) -> Optional[User]:
|
||||||
|
if not credentials:
|
||||||
|
return None
|
||||||
|
token = credentials.credentials
|
||||||
|
try:
|
||||||
|
jwt_secret = getattr(settings, 'JWT_SECRET', None) or os.getenv('JWT_SECRET', 'dev-secret-key-change-in-production-12345')
|
||||||
|
payload = jwt.decode(token, jwt_secret, algorithms=['HS256'])
|
||||||
|
user_id: int = payload.get('userId')
|
||||||
|
if user_id is None:
|
||||||
|
return None
|
||||||
|
except JWTError:
|
||||||
|
return None
|
||||||
|
user = db.query(User).filter(User.id == user_id).first()
|
||||||
|
return user
|
||||||
|
|
||||||
|
def verify_token(token: str) -> dict:
|
||||||
|
jwt_secret = getattr(settings, 'JWT_SECRET', None) or os.getenv('JWT_SECRET', 'dev-secret-key-change-in-production-12345')
|
||||||
|
payload = jwt.decode(token, jwt_secret, algorithms=['HS256'])
|
||||||
|
return payload
|
||||||
@@ -1,89 +1,52 @@
|
|||||||
import json
|
import json
|
||||||
from typing import Callable, Awaitable
|
from typing import Callable, Awaitable
|
||||||
|
|
||||||
from fastapi import Request, Response
|
from fastapi import Request, Response
|
||||||
from starlette.middleware.base import BaseHTTPMiddleware
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
|
|
||||||
from ..schemas.privacy import CookieConsent, CookieCategoryPreferences
|
from ..schemas.privacy import CookieConsent, CookieCategoryPreferences
|
||||||
from ..config.settings import settings
|
from ..config.settings import settings
|
||||||
from ..config.logging_config import get_logger
|
from ..config.logging_config import get_logger
|
||||||
|
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
COOKIE_CONSENT_COOKIE_NAME = 'cookieConsent'
|
||||||
|
|
||||||
COOKIE_CONSENT_COOKIE_NAME = "cookieConsent"
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_consent_cookie(raw_value: str | None) -> CookieConsent:
|
def _parse_consent_cookie(raw_value: str | None) -> CookieConsent:
|
||||||
if not raw_value:
|
if not raw_value:
|
||||||
return CookieConsent() # Defaults: only necessary = True
|
return CookieConsent()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = json.loads(raw_value)
|
data = json.loads(raw_value)
|
||||||
# Pydantic will validate and coerce as needed
|
|
||||||
return CookieConsent(**data)
|
return CookieConsent(**data)
|
||||||
except Exception as exc: # pragma: no cover - defensive
|
except Exception as exc:
|
||||||
logger.warning(f"Failed to parse cookie consent cookie: {exc}")
|
logger.warning(f'Failed to parse cookie consent cookie: {exc}')
|
||||||
return CookieConsent()
|
return CookieConsent()
|
||||||
|
|
||||||
|
|
||||||
class CookieConsentMiddleware(BaseHTTPMiddleware):
|
class CookieConsentMiddleware(BaseHTTPMiddleware):
|
||||||
"""
|
|
||||||
Middleware that parses the cookie consent cookie (if present) and attaches it
|
|
||||||
to `request.state.cookie_consent` for downstream handlers.
|
|
||||||
"""
|
|
||||||
|
|
||||||
async def dispatch(
|
async def dispatch(self, request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
|
||||||
self, request: Request, call_next: Callable[[Request], Awaitable[Response]]
|
|
||||||
) -> Response:
|
|
||||||
raw_cookie = request.cookies.get(COOKIE_CONSENT_COOKIE_NAME)
|
raw_cookie = request.cookies.get(COOKIE_CONSENT_COOKIE_NAME)
|
||||||
consent = _parse_consent_cookie(raw_cookie)
|
consent = _parse_consent_cookie(raw_cookie)
|
||||||
|
|
||||||
# Ensure 'necessary' is always true regardless of stored value
|
|
||||||
consent.categories.necessary = True
|
consent.categories.necessary = True
|
||||||
|
|
||||||
request.state.cookie_consent = consent
|
request.state.cookie_consent = consent
|
||||||
|
|
||||||
response = await call_next(request)
|
response = await call_next(request)
|
||||||
|
|
||||||
# If there's no cookie yet, set a minimal default consent cookie
|
|
||||||
# so that the banner can be rendered based on server-side knowledge.
|
|
||||||
if COOKIE_CONSENT_COOKIE_NAME not in request.cookies:
|
if COOKIE_CONSENT_COOKIE_NAME not in request.cookies:
|
||||||
try:
|
try:
|
||||||
response.set_cookie(
|
response.set_cookie(key=COOKIE_CONSENT_COOKIE_NAME, value=consent.model_dump_json(), httponly=True, secure=settings.is_production, samesite='lax', max_age=365 * 24 * 60 * 60, path='/')
|
||||||
key=COOKIE_CONSENT_COOKIE_NAME,
|
except Exception as exc:
|
||||||
value=consent.model_dump_json(),
|
logger.warning(f'Failed to set default cookie consent cookie: {exc}')
|
||||||
httponly=True,
|
|
||||||
secure=settings.is_production,
|
|
||||||
samesite="lax",
|
|
||||||
max_age=365 * 24 * 60 * 60, # 1 year
|
|
||||||
path="/",
|
|
||||||
)
|
|
||||||
except Exception as exc: # pragma: no cover - defensive
|
|
||||||
logger.warning(f"Failed to set default cookie consent cookie: {exc}")
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
def is_analytics_allowed(request: Request) -> bool:
|
def is_analytics_allowed(request: Request) -> bool:
|
||||||
consent: CookieConsent | None = getattr(request.state, "cookie_consent", None)
|
consent: CookieConsent | None = getattr(request.state, 'cookie_consent', None)
|
||||||
if not consent:
|
if not consent:
|
||||||
return False
|
return False
|
||||||
return consent.categories.analytics
|
return consent.categories.analytics
|
||||||
|
|
||||||
|
|
||||||
def is_marketing_allowed(request: Request) -> bool:
|
def is_marketing_allowed(request: Request) -> bool:
|
||||||
consent: CookieConsent | None = getattr(request.state, "cookie_consent", None)
|
consent: CookieConsent | None = getattr(request.state, 'cookie_consent', None)
|
||||||
if not consent:
|
if not consent:
|
||||||
return False
|
return False
|
||||||
return consent.categories.marketing
|
return consent.categories.marketing
|
||||||
|
|
||||||
|
|
||||||
def is_preferences_allowed(request: Request) -> bool:
|
def is_preferences_allowed(request: Request) -> bool:
|
||||||
consent: CookieConsent | None = getattr(request.state, "cookie_consent", None)
|
consent: CookieConsent | None = getattr(request.state, 'cookie_consent', None)
|
||||||
if not consent:
|
if not consent:
|
||||||
return False
|
return False
|
||||||
return consent.categories.preferences
|
return consent.categories.preferences
|
||||||
|
|
||||||
|
|
||||||
@@ -5,140 +5,47 @@ from sqlalchemy.exc import IntegrityError
|
|||||||
from jose.exceptions import JWTError
|
from jose.exceptions import JWTError
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
|
||||||
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
||||||
"""
|
|
||||||
Handle validation errors
|
|
||||||
"""
|
|
||||||
errors = []
|
errors = []
|
||||||
for error in exc.errors():
|
for error in exc.errors():
|
||||||
field = ".".join(str(loc) for loc in error["loc"] if loc != "body")
|
field = '.'.join((str(loc) for loc in error['loc'] if loc != 'body'))
|
||||||
errors.append({
|
errors.append({'field': field, 'message': error['msg']})
|
||||||
"field": field,
|
first_error = errors[0]['message'] if errors else 'Validation error'
|
||||||
"message": error["msg"]
|
return JSONResponse(status_code=status.HTTP_400_BAD_REQUEST, content={'status': 'error', 'message': first_error, 'errors': errors})
|
||||||
})
|
|
||||||
|
|
||||||
# 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):
|
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)
|
error_msg = str(exc.orig) if hasattr(exc, 'orig') else str(exc)
|
||||||
|
if 'Duplicate entry' in error_msg or 'UNIQUE constraint' in error_msg:
|
||||||
# Check for duplicate entry
|
return JSONResponse(status_code=status.HTTP_400_BAD_REQUEST, content={'status': 'error', 'message': 'Duplicate entry', 'errors': [{'message': 'This record already exists'}]})
|
||||||
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': 'Database integrity error'})
|
||||||
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):
|
async def jwt_error_handler(request: Request, exc: JWTError):
|
||||||
"""
|
return JSONResponse(status_code=status.HTTP_401_UNAUTHORIZED, content={'status': 'error', 'message': 'Invalid token'})
|
||||||
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):
|
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):
|
if isinstance(exc.detail, dict):
|
||||||
return JSONResponse(
|
return JSONResponse(status_code=exc.status_code, content=exc.detail)
|
||||||
status_code=exc.status_code,
|
return JSONResponse(status_code=exc.status_code, content={'status': 'error', 'message': str(exc.detail) if exc.detail else 'An error occurred'})
|
||||||
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):
|
async def general_exception_handler(request: Request, exc: Exception):
|
||||||
"""
|
|
||||||
Handle all other exceptions
|
|
||||||
"""
|
|
||||||
from ..config.logging_config import get_logger
|
from ..config.logging_config import get_logger
|
||||||
from ..config.settings import settings
|
from ..config.settings import settings
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
request_id = getattr(request.state, "request_id", None)
|
request_id = getattr(request.state, 'request_id', None)
|
||||||
|
logger.error(f'Unhandled exception: {type(exc).__name__}: {str(exc)}', extra={'request_id': request_id, 'path': request.url.path, 'method': request.method, 'exception_type': type(exc).__name__}, exc_info=True)
|
||||||
# Log error with context
|
if isinstance(exc, Exception) and hasattr(exc, 'status_code'):
|
||||||
logger.error(
|
|
||||||
f"Unhandled exception: {type(exc).__name__}: {str(exc)}",
|
|
||||||
extra={
|
|
||||||
"request_id": request_id,
|
|
||||||
"path": request.url.path,
|
|
||||||
"method": request.method,
|
|
||||||
"exception_type": type(exc).__name__
|
|
||||||
},
|
|
||||||
exc_info=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Handle HTTPException with dict detail
|
|
||||||
if isinstance(exc, Exception) and hasattr(exc, "status_code"):
|
|
||||||
status_code = exc.status_code
|
status_code = exc.status_code
|
||||||
if hasattr(exc, "detail"):
|
if hasattr(exc, 'detail'):
|
||||||
detail = exc.detail
|
detail = exc.detail
|
||||||
if isinstance(detail, dict):
|
if isinstance(detail, dict):
|
||||||
# If detail is already a dict with status/message, return it directly
|
|
||||||
return JSONResponse(status_code=status_code, content=detail)
|
return JSONResponse(status_code=status_code, content=detail)
|
||||||
message = str(detail) if detail else "An error occurred"
|
message = str(detail) if detail else 'An error occurred'
|
||||||
else:
|
else:
|
||||||
message = str(exc) if str(exc) else "Internal server error"
|
message = str(exc) if str(exc) else 'Internal server error'
|
||||||
else:
|
else:
|
||||||
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
|
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||||
message = str(exc) if str(exc) else "Internal server error"
|
message = str(exc) if str(exc) else 'Internal server error'
|
||||||
|
response_content = {'status': 'error', 'message': message}
|
||||||
response_content = {
|
|
||||||
"status": "error",
|
|
||||||
"message": message
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add stack trace in development
|
|
||||||
if settings.is_development:
|
if settings.is_development:
|
||||||
response_content["stack"] = traceback.format_exc()
|
response_content['stack'] = traceback.format_exc()
|
||||||
|
return JSONResponse(status_code=status_code, content=response_content)
|
||||||
return JSONResponse(
|
|
||||||
status_code=status_code,
|
|
||||||
content=response_content
|
|
||||||
)
|
|
||||||
|
|
||||||
@@ -1,65 +1,21 @@
|
|||||||
"""
|
|
||||||
Request ID middleware for tracking requests across services
|
|
||||||
"""
|
|
||||||
import uuid
|
import uuid
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
from starlette.middleware.base import BaseHTTPMiddleware
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
from starlette.responses import Response
|
from starlette.responses import Response
|
||||||
from ..config.logging_config import get_logger
|
from ..config.logging_config import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class RequestIDMiddleware(BaseHTTPMiddleware):
|
class RequestIDMiddleware(BaseHTTPMiddleware):
|
||||||
"""Add unique request ID to each request for tracing"""
|
|
||||||
|
|
||||||
async def dispatch(self, request: Request, call_next):
|
async def dispatch(self, request: Request, call_next):
|
||||||
# Generate or get request ID
|
request_id = request.headers.get('X-Request-ID') or str(uuid.uuid4())
|
||||||
request_id = request.headers.get("X-Request-ID") or str(uuid.uuid4())
|
|
||||||
|
|
||||||
# Add request ID to request state
|
|
||||||
request.state.request_id = request_id
|
request.state.request_id = request_id
|
||||||
|
logger.info(f'Request started: {request.method} {request.url.path}', extra={'request_id': request_id, 'method': request.method, 'path': request.url.path, 'client_ip': request.client.host if request.client else None})
|
||||||
# Log request
|
|
||||||
logger.info(
|
|
||||||
f"Request started: {request.method} {request.url.path}",
|
|
||||||
extra={
|
|
||||||
"request_id": request_id,
|
|
||||||
"method": request.method,
|
|
||||||
"path": request.url.path,
|
|
||||||
"client_ip": request.client.host if request.client else None
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Process request
|
|
||||||
try:
|
try:
|
||||||
response = await call_next(request)
|
response = await call_next(request)
|
||||||
|
response.headers['X-Request-ID'] = request_id
|
||||||
# Add request ID to response headers
|
logger.info(f'Request completed: {request.method} {request.url.path} - {response.status_code}', extra={'request_id': request_id, 'method': request.method, 'path': request.url.path, 'status_code': response.status_code})
|
||||||
response.headers["X-Request-ID"] = request_id
|
|
||||||
|
|
||||||
# Log response
|
|
||||||
logger.info(
|
|
||||||
f"Request completed: {request.method} {request.url.path} - {response.status_code}",
|
|
||||||
extra={
|
|
||||||
"request_id": request_id,
|
|
||||||
"method": request.method,
|
|
||||||
"path": request.url.path,
|
|
||||||
"status_code": response.status_code
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(
|
logger.error(f'Request failed: {request.method} {request.url.path} - {str(e)}', extra={'request_id': request_id, 'method': request.method, 'path': request.url.path, 'error': str(e)}, exc_info=True)
|
||||||
f"Request failed: {request.method} {request.url.path} - {str(e)}",
|
raise
|
||||||
extra={
|
|
||||||
"request_id": request_id,
|
|
||||||
"method": request.method,
|
|
||||||
"path": request.url.path,
|
|
||||||
"error": str(e)
|
|
||||||
},
|
|
||||||
exc_info=True
|
|
||||||
)
|
|
||||||
raise
|
|
||||||
|
|
||||||
@@ -1,57 +1,20 @@
|
|||||||
"""
|
|
||||||
Security middleware for adding security headers
|
|
||||||
"""
|
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
from starlette.middleware.base import BaseHTTPMiddleware
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
from starlette.responses import Response
|
from starlette.responses import Response
|
||||||
from ..config.logging_config import get_logger
|
from ..config.logging_config import get_logger
|
||||||
from ..config.settings import settings
|
from ..config.settings import settings
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
||||||
"""Add security headers to all responses"""
|
|
||||||
|
|
||||||
async def dispatch(self, request: Request, call_next):
|
async def dispatch(self, request: Request, call_next):
|
||||||
response = await call_next(request)
|
response = await call_next(request)
|
||||||
|
security_headers = {'X-Content-Type-Options': 'nosniff', 'X-Frame-Options': 'DENY', 'X-XSS-Protection': '1; mode=block', 'Referrer-Policy': 'strict-origin-when-cross-origin', 'Permissions-Policy': 'geolocation=(), microphone=(), camera=()'}
|
||||||
# Security headers
|
security_headers.setdefault('Cross-Origin-Resource-Policy', 'cross-origin')
|
||||||
security_headers = {
|
|
||||||
"X-Content-Type-Options": "nosniff",
|
|
||||||
"X-Frame-Options": "DENY",
|
|
||||||
"X-XSS-Protection": "1; mode=block",
|
|
||||||
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
||||||
"Permissions-Policy": "geolocation=(), microphone=(), camera=()",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Allow resources (like banner images) to be loaded cross-origin by the frontend.
|
|
||||||
# This helps avoid Firefox's OpaqueResponseBlocking when the frontend runs
|
|
||||||
# on a different origin (e.g. Vite dev server on :5173) and loads images
|
|
||||||
# from the API origin (e.g. :8000).
|
|
||||||
#
|
|
||||||
# In production you may want a stricter policy (e.g. "same-site") depending
|
|
||||||
# on your deployment topology.
|
|
||||||
security_headers.setdefault("Cross-Origin-Resource-Policy", "cross-origin")
|
|
||||||
|
|
||||||
# Add Content-Security-Policy
|
|
||||||
if settings.is_production:
|
if settings.is_production:
|
||||||
security_headers["Content-Security-Policy"] = (
|
security_headers['Content-Security-Policy'] = "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'"
|
||||||
"default-src 'self'; "
|
|
||||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; "
|
|
||||||
"style-src 'self' 'unsafe-inline'; "
|
|
||||||
"img-src 'self' data: https:; "
|
|
||||||
"font-src 'self' data:; "
|
|
||||||
"connect-src 'self'"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add Strict-Transport-Security in production with HTTPS
|
|
||||||
if settings.is_production:
|
if settings.is_production:
|
||||||
security_headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
|
security_headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
|
||||||
|
|
||||||
# Apply headers
|
|
||||||
for header, value in security_headers.items():
|
for header, value in security_headers.items():
|
||||||
response.headers[header] = value
|
response.headers[header] = value
|
||||||
|
return response
|
||||||
return response
|
|
||||||
|
|
||||||
@@ -1,41 +1,16 @@
|
|||||||
"""
|
|
||||||
Request timeout middleware
|
|
||||||
"""
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from fastapi import Request, HTTPException, status
|
from fastapi import Request, HTTPException, status
|
||||||
from starlette.middleware.base import BaseHTTPMiddleware
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
from ..config.logging_config import get_logger
|
from ..config.logging_config import get_logger
|
||||||
from ..config.settings import settings
|
from ..config.settings import settings
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TimeoutMiddleware(BaseHTTPMiddleware):
|
class TimeoutMiddleware(BaseHTTPMiddleware):
|
||||||
"""Add timeout to requests"""
|
|
||||||
|
|
||||||
async def dispatch(self, request: Request, call_next):
|
async def dispatch(self, request: Request, call_next):
|
||||||
try:
|
try:
|
||||||
# Use asyncio.wait_for to add timeout
|
response = await asyncio.wait_for(call_next(request), timeout=settings.REQUEST_TIMEOUT)
|
||||||
response = await asyncio.wait_for(
|
|
||||||
call_next(request),
|
|
||||||
timeout=settings.REQUEST_TIMEOUT
|
|
||||||
)
|
|
||||||
return response
|
return response
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
logger.warning(
|
logger.warning(f'Request timeout: {request.method} {request.url.path}', extra={'request_id': getattr(request.state, 'request_id', None), 'method': request.method, 'path': request.url.path, 'timeout': settings.REQUEST_TIMEOUT})
|
||||||
f"Request timeout: {request.method} {request.url.path}",
|
raise HTTPException(status_code=status.HTTP_504_GATEWAY_TIMEOUT, detail={'status': 'error', 'message': 'Request timeout. Please try again.'})
|
||||||
extra={
|
|
||||||
"request_id": getattr(request.state, "request_id", None),
|
|
||||||
"method": request.method,
|
|
||||||
"path": request.url.path,
|
|
||||||
"timeout": settings.REQUEST_TIMEOUT
|
|
||||||
}
|
|
||||||
)
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_504_GATEWAY_TIMEOUT,
|
|
||||||
detail={
|
|
||||||
"status": "error",
|
|
||||||
"message": "Request timeout. Please try again."
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
@@ -20,36 +20,5 @@ from .cookie_integration_config import CookieIntegrationConfig
|
|||||||
from .system_settings import SystemSettings
|
from .system_settings import SystemSettings
|
||||||
from .invoice import Invoice, InvoiceItem
|
from .invoice import Invoice, InvoiceItem
|
||||||
from .page_content import PageContent, PageType
|
from .page_content import PageContent, PageType
|
||||||
|
from .chat import Chat, ChatMessage, ChatStatus
|
||||||
__all__ = [
|
__all__ = ['Role', 'User', 'RefreshToken', 'PasswordResetToken', 'RoomType', 'Room', 'Booking', 'Payment', 'Service', 'ServiceUsage', 'ServiceBooking', 'ServiceBookingItem', 'ServicePayment', 'ServiceBookingStatus', 'ServicePaymentStatus', 'ServicePaymentMethod', 'Promotion', 'CheckInCheckOut', 'Banner', 'Review', 'Favorite', 'AuditLog', 'CookiePolicy', 'CookieIntegrationConfig', 'SystemSettings', 'Invoice', 'InvoiceItem', 'PageContent', 'PageType', 'Chat', 'ChatMessage', 'ChatStatus']
|
||||||
"Role",
|
|
||||||
"User",
|
|
||||||
"RefreshToken",
|
|
||||||
"PasswordResetToken",
|
|
||||||
"RoomType",
|
|
||||||
"Room",
|
|
||||||
"Booking",
|
|
||||||
"Payment",
|
|
||||||
"Service",
|
|
||||||
"ServiceUsage",
|
|
||||||
"ServiceBooking",
|
|
||||||
"ServiceBookingItem",
|
|
||||||
"ServicePayment",
|
|
||||||
"ServiceBookingStatus",
|
|
||||||
"ServicePaymentStatus",
|
|
||||||
"ServicePaymentMethod",
|
|
||||||
"Promotion",
|
|
||||||
"CheckInCheckOut",
|
|
||||||
"Banner",
|
|
||||||
"Review",
|
|
||||||
"Favorite",
|
|
||||||
"AuditLog",
|
|
||||||
"CookiePolicy",
|
|
||||||
"CookieIntegrationConfig",
|
|
||||||
"SystemSettings",
|
|
||||||
"Invoice",
|
|
||||||
"InvoiceItem",
|
|
||||||
"PageContent",
|
|
||||||
"PageType",
|
|
||||||
]
|
|
||||||
|
|
||||||
Binary file not shown.
BIN
Backend/src/models/__pycache__/chat.cpython-312.pyc
Normal file
BIN
Backend/src/models/__pycache__/chat.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
@@ -1,28 +1,20 @@
|
|||||||
"""
|
|
||||||
Audit log model for tracking important actions
|
|
||||||
"""
|
|
||||||
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, JSON
|
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, JSON
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from ..config.database import Base
|
from ..config.database import Base
|
||||||
|
|
||||||
|
|
||||||
class AuditLog(Base):
|
class AuditLog(Base):
|
||||||
__tablename__ = "audit_logs"
|
__tablename__ = 'audit_logs'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=True, index=True)
|
user_id = Column(Integer, ForeignKey('users.id'), nullable=True, index=True)
|
||||||
action = Column(String(100), nullable=False, index=True) # e.g., "user.created", "booking.cancelled"
|
action = Column(String(100), nullable=False, index=True)
|
||||||
resource_type = Column(String(50), nullable=False, index=True) # e.g., "user", "booking"
|
resource_type = Column(String(50), nullable=False, index=True)
|
||||||
resource_id = Column(Integer, nullable=True, index=True)
|
resource_id = Column(Integer, nullable=True, index=True)
|
||||||
ip_address = Column(String(45), nullable=True) # IPv6 compatible
|
ip_address = Column(String(45), nullable=True)
|
||||||
user_agent = Column(String(255), nullable=True)
|
user_agent = Column(String(255), nullable=True)
|
||||||
request_id = Column(String(36), nullable=True, index=True) # UUID
|
request_id = Column(String(36), nullable=True, index=True)
|
||||||
details = Column(JSON, nullable=True) # Additional context
|
details = Column(JSON, nullable=True)
|
||||||
status = Column(String(20), nullable=False, default="success") # success, failed, error
|
status = Column(String(20), nullable=False, default='success')
|
||||||
error_message = Column(Text, nullable=True)
|
error_message = Column(Text, nullable=True)
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False, index=True)
|
created_at = Column(DateTime, default=datetime.utcnow, nullable=False, index=True)
|
||||||
|
user = relationship('User', foreign_keys=[user_id])
|
||||||
# Relationships
|
|
||||||
user = relationship("User", foreign_keys=[user_id])
|
|
||||||
|
|
||||||
@@ -3,16 +3,14 @@ from sqlalchemy.orm import relationship
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from ..config.database import Base
|
from ..config.database import Base
|
||||||
|
|
||||||
|
|
||||||
class Banner(Base):
|
class Banner(Base):
|
||||||
__tablename__ = "banners"
|
__tablename__ = 'banners'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
title = Column(String(100), nullable=False)
|
title = Column(String(100), nullable=False)
|
||||||
description = Column(Text, nullable=True)
|
description = Column(Text, nullable=True)
|
||||||
image_url = Column(String(255), nullable=False)
|
image_url = Column(String(255), nullable=False)
|
||||||
link_url = Column(String(255), nullable=True)
|
link_url = Column(String(255), nullable=True)
|
||||||
position = Column(String(50), nullable=False, default="home")
|
position = Column(String(50), nullable=False, default='home')
|
||||||
display_order = Column(Integer, nullable=False, default=0)
|
display_order = Column(Integer, nullable=False, default=0)
|
||||||
is_active = Column(Boolean, nullable=False, default=True)
|
is_active = Column(Boolean, nullable=False, default=True)
|
||||||
start_date = Column(DateTime, nullable=True)
|
start_date = Column(DateTime, nullable=True)
|
||||||
@@ -27,5 +25,4 @@ class Banner(Base):
|
|||||||
return False
|
return False
|
||||||
if not self.start_date or not self.end_date:
|
if not self.start_date or not self.end_date:
|
||||||
return self.is_active
|
return self.is_active
|
||||||
return self.start_date <= now <= self.end_date
|
return self.start_date <= now <= self.end_date
|
||||||
|
|
||||||
@@ -4,41 +4,35 @@ from datetime import datetime
|
|||||||
import enum
|
import enum
|
||||||
from ..config.database import Base
|
from ..config.database import Base
|
||||||
|
|
||||||
|
|
||||||
class BookingStatus(str, enum.Enum):
|
class BookingStatus(str, enum.Enum):
|
||||||
pending = "pending"
|
pending = 'pending'
|
||||||
confirmed = "confirmed"
|
confirmed = 'confirmed'
|
||||||
checked_in = "checked_in"
|
checked_in = 'checked_in'
|
||||||
checked_out = "checked_out"
|
checked_out = 'checked_out'
|
||||||
cancelled = "cancelled"
|
cancelled = 'cancelled'
|
||||||
|
|
||||||
|
|
||||||
class Booking(Base):
|
class Booking(Base):
|
||||||
__tablename__ = "bookings"
|
__tablename__ = 'bookings'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
booking_number = Column(String(50), unique=True, nullable=False, index=True)
|
booking_number = Column(String(50), unique=True, nullable=False, index=True)
|
||||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
|
||||||
room_id = Column(Integer, ForeignKey("rooms.id"), nullable=False)
|
room_id = Column(Integer, ForeignKey('rooms.id'), nullable=False)
|
||||||
check_in_date = Column(DateTime, nullable=False)
|
check_in_date = Column(DateTime, nullable=False)
|
||||||
check_out_date = Column(DateTime, nullable=False)
|
check_out_date = Column(DateTime, nullable=False)
|
||||||
num_guests = Column(Integer, nullable=False, default=1)
|
num_guests = Column(Integer, nullable=False, default=1)
|
||||||
total_price = Column(Numeric(10, 2), nullable=False)
|
total_price = Column(Numeric(10, 2), nullable=False)
|
||||||
original_price = Column(Numeric(10, 2), nullable=True) # Price before discount
|
original_price = Column(Numeric(10, 2), nullable=True)
|
||||||
discount_amount = Column(Numeric(10, 2), nullable=True, default=0) # Discount amount applied
|
discount_amount = Column(Numeric(10, 2), nullable=True, default=0)
|
||||||
promotion_code = Column(String(50), nullable=True) # Promotion code used
|
promotion_code = Column(String(50), nullable=True)
|
||||||
status = Column(Enum(BookingStatus), nullable=False, default=BookingStatus.pending)
|
status = Column(Enum(BookingStatus), nullable=False, default=BookingStatus.pending)
|
||||||
deposit_paid = Column(Boolean, nullable=False, default=False)
|
deposit_paid = Column(Boolean, nullable=False, default=False)
|
||||||
requires_deposit = Column(Boolean, nullable=False, default=False)
|
requires_deposit = Column(Boolean, nullable=False, default=False)
|
||||||
special_requests = Column(Text, nullable=True)
|
special_requests = Column(Text, nullable=True)
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||||
|
user = relationship('User', back_populates='bookings')
|
||||||
# Relationships
|
room = relationship('Room', back_populates='bookings')
|
||||||
user = relationship("User", back_populates="bookings")
|
payments = relationship('Payment', back_populates='booking', cascade='all, delete-orphan')
|
||||||
room = relationship("Room", back_populates="bookings")
|
invoices = relationship('Invoice', back_populates='booking', cascade='all, delete-orphan')
|
||||||
payments = relationship("Payment", back_populates="booking", cascade="all, delete-orphan")
|
service_usages = relationship('ServiceUsage', back_populates='booking', cascade='all, delete-orphan')
|
||||||
invoices = relationship("Invoice", back_populates="booking", cascade="all, delete-orphan")
|
checkin_checkout = relationship('CheckInCheckOut', back_populates='booking', uselist=False)
|
||||||
service_usages = relationship("ServiceUsage", back_populates="booking", cascade="all, delete-orphan")
|
|
||||||
checkin_checkout = relationship("CheckInCheckOut", back_populates="booking", uselist=False)
|
|
||||||
|
|
||||||
37
Backend/src/models/chat.py
Normal file
37
Backend/src/models/chat.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
from sqlalchemy import Column, Integer, String, Text, ForeignKey, DateTime, Enum, Boolean
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from datetime import datetime
|
||||||
|
import enum
|
||||||
|
from ..config.database import Base
|
||||||
|
|
||||||
|
class ChatStatus(str, enum.Enum):
|
||||||
|
pending = 'pending'
|
||||||
|
active = 'active'
|
||||||
|
closed = 'closed'
|
||||||
|
|
||||||
|
class Chat(Base):
|
||||||
|
__tablename__ = 'chats'
|
||||||
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
|
visitor_id = Column(Integer, ForeignKey('users.id'), nullable=True)
|
||||||
|
visitor_name = Column(String(100), nullable=True)
|
||||||
|
visitor_email = Column(String(100), nullable=True)
|
||||||
|
staff_id = Column(Integer, ForeignKey('users.id'), nullable=True)
|
||||||
|
status = Column(Enum(ChatStatus), nullable=False, default=ChatStatus.pending)
|
||||||
|
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||||
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||||
|
closed_at = Column(DateTime, nullable=True)
|
||||||
|
visitor = relationship('User', foreign_keys=[visitor_id], back_populates='visitor_chats')
|
||||||
|
staff = relationship('User', foreign_keys=[staff_id], back_populates='staff_chats')
|
||||||
|
messages = relationship('ChatMessage', back_populates='chat', cascade='all, delete-orphan', order_by='ChatMessage.created_at')
|
||||||
|
|
||||||
|
class ChatMessage(Base):
|
||||||
|
__tablename__ = 'chat_messages'
|
||||||
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
|
chat_id = Column(Integer, ForeignKey('chats.id'), nullable=False)
|
||||||
|
sender_id = Column(Integer, ForeignKey('users.id'), nullable=True)
|
||||||
|
sender_type = Column(String(20), nullable=False)
|
||||||
|
message = Column(Text, nullable=False)
|
||||||
|
is_read = Column(Boolean, nullable=False, default=False)
|
||||||
|
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||||
|
chat = relationship('Chat', back_populates='messages')
|
||||||
|
sender = relationship('User', foreign_keys=[sender_id])
|
||||||
@@ -3,25 +3,20 @@ from sqlalchemy.orm import relationship
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from ..config.database import Base
|
from ..config.database import Base
|
||||||
|
|
||||||
|
|
||||||
class CheckInCheckOut(Base):
|
class CheckInCheckOut(Base):
|
||||||
__tablename__ = "checkin_checkout"
|
__tablename__ = 'checkin_checkout'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
booking_id = Column(Integer, ForeignKey("bookings.id"), nullable=False, unique=True)
|
booking_id = Column(Integer, ForeignKey('bookings.id'), nullable=False, unique=True)
|
||||||
checkin_time = Column(DateTime, nullable=True)
|
checkin_time = Column(DateTime, nullable=True)
|
||||||
checkout_time = Column(DateTime, nullable=True)
|
checkout_time = Column(DateTime, nullable=True)
|
||||||
checkin_by = Column(Integer, ForeignKey("users.id"), nullable=True)
|
checkin_by = Column(Integer, ForeignKey('users.id'), nullable=True)
|
||||||
checkout_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_checkin = Column(Text, nullable=True)
|
||||||
room_condition_checkout = Column(Text, nullable=True)
|
room_condition_checkout = Column(Text, nullable=True)
|
||||||
additional_charges = Column(Numeric(10, 2), nullable=False, default=0.0)
|
additional_charges = Column(Numeric(10, 2), nullable=False, default=0.0)
|
||||||
notes = Column(Text, nullable=True)
|
notes = Column(Text, nullable=True)
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||||
|
booking = relationship('Booking', back_populates='checkin_checkout')
|
||||||
# Relationships
|
checked_in_by = relationship('User', foreign_keys=[checkin_by], back_populates='checkins_processed')
|
||||||
booking = relationship("Booking", back_populates="checkin_checkout")
|
checked_out_by = relationship('User', foreign_keys=[checkout_by], back_populates='checkouts_processed')
|
||||||
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")
|
|
||||||
|
|
||||||
@@ -1,30 +1,14 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String
|
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from ..config.database import Base
|
from ..config.database import Base
|
||||||
|
|
||||||
|
|
||||||
class CookieIntegrationConfig(Base):
|
class CookieIntegrationConfig(Base):
|
||||||
"""
|
__tablename__ = 'cookie_integration_configs'
|
||||||
Stores IDs for well-known integrations (e.g., Google Analytics, Meta Pixel).
|
|
||||||
Does NOT allow arbitrary script injection from the dashboard.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__tablename__ = "cookie_integration_configs"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
|
ga_measurement_id = Column(String(64), nullable=True)
|
||||||
ga_measurement_id = Column(String(64), nullable=True) # e.g. G-XXXXXXXXXX
|
fb_pixel_id = Column(String(64), nullable=True)
|
||||||
fb_pixel_id = Column(String(64), nullable=True) # e.g. 1234567890
|
|
||||||
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||||
updated_at = Column(
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||||
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False
|
updated_by_id = Column(Integer, ForeignKey('users.id'), nullable=True)
|
||||||
)
|
updated_by = relationship('User', lazy='joined')
|
||||||
|
|
||||||
updated_by_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
|
||||||
updated_by = relationship("User", lazy="joined")
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,31 +1,15 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer
|
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from ..config.database import Base
|
from ..config.database import Base
|
||||||
|
|
||||||
|
|
||||||
class CookiePolicy(Base):
|
class CookiePolicy(Base):
|
||||||
"""
|
__tablename__ = 'cookie_policies'
|
||||||
Global cookie policy controlled by administrators.
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
|
analytics_enabled = Column(Boolean, default=True, nullable=False)
|
||||||
This does NOT store per-user consent; it controls which cookie categories
|
marketing_enabled = Column(Boolean, default=True, nullable=False)
|
||||||
are available to be requested from users (e.g., disable analytics entirely).
|
preferences_enabled = Column(Boolean, default=True, nullable=False)
|
||||||
"""
|
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||||
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||||
__tablename__ = "cookie_policies"
|
updated_by_id = Column(Integer, ForeignKey('users.id'), nullable=True)
|
||||||
|
updated_by = relationship('User', lazy='joined')
|
||||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
|
||||||
|
|
||||||
analytics_enabled = Column(Boolean, default=True, nullable=False)
|
|
||||||
marketing_enabled = Column(Boolean, default=True, nullable=False)
|
|
||||||
preferences_enabled = Column(Boolean, default=True, nullable=False)
|
|
||||||
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
|
||||||
|
|
||||||
updated_by_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
|
||||||
updated_by = relationship("User", lazy="joined")
|
|
||||||
|
|
||||||
|
|
||||||
@@ -3,17 +3,12 @@ from sqlalchemy.orm import relationship
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from ..config.database import Base
|
from ..config.database import Base
|
||||||
|
|
||||||
|
|
||||||
class Favorite(Base):
|
class Favorite(Base):
|
||||||
__tablename__ = "favorites"
|
__tablename__ = 'favorites'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
|
||||||
room_id = Column(Integer, ForeignKey("rooms.id"), nullable=False)
|
room_id = Column(Integer, ForeignKey('rooms.id'), nullable=False)
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||||
|
user = relationship('User', back_populates='favorites')
|
||||||
# Relationships
|
room = relationship('Room', back_populates='favorites')
|
||||||
user = relationship("User", back_populates="favorites")
|
|
||||||
room = relationship("Room", back_populates="favorites")
|
|
||||||
|
|
||||||
@@ -4,98 +4,68 @@ from datetime import datetime
|
|||||||
import enum
|
import enum
|
||||||
from ..config.database import Base
|
from ..config.database import Base
|
||||||
|
|
||||||
|
|
||||||
class InvoiceStatus(str, enum.Enum):
|
class InvoiceStatus(str, enum.Enum):
|
||||||
draft = "draft"
|
draft = 'draft'
|
||||||
sent = "sent"
|
sent = 'sent'
|
||||||
paid = "paid"
|
paid = 'paid'
|
||||||
overdue = "overdue"
|
overdue = 'overdue'
|
||||||
cancelled = "cancelled"
|
cancelled = 'cancelled'
|
||||||
|
|
||||||
|
|
||||||
class Invoice(Base):
|
class Invoice(Base):
|
||||||
__tablename__ = "invoices"
|
__tablename__ = 'invoices'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
invoice_number = Column(String(50), unique=True, nullable=False, index=True)
|
invoice_number = Column(String(50), unique=True, nullable=False, index=True)
|
||||||
booking_id = Column(Integer, ForeignKey("bookings.id"), nullable=False)
|
booking_id = Column(Integer, ForeignKey('bookings.id'), nullable=False)
|
||||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
|
||||||
|
|
||||||
# Invoice details
|
|
||||||
issue_date = Column(DateTime, default=datetime.utcnow, nullable=False)
|
issue_date = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||||
due_date = Column(DateTime, nullable=False)
|
due_date = Column(DateTime, nullable=False)
|
||||||
paid_date = Column(DateTime, nullable=True)
|
paid_date = Column(DateTime, nullable=True)
|
||||||
|
subtotal = Column(Numeric(10, 2), nullable=False, default=0.0)
|
||||||
# Amounts
|
tax_rate = Column(Numeric(5, 2), nullable=False, default=0.0)
|
||||||
subtotal = Column(Numeric(10, 2), nullable=False, default=0.00)
|
tax_amount = Column(Numeric(10, 2), nullable=False, default=0.0)
|
||||||
tax_rate = Column(Numeric(5, 2), nullable=False, default=0.00) # Tax percentage
|
discount_amount = Column(Numeric(10, 2), nullable=False, default=0.0)
|
||||||
tax_amount = Column(Numeric(10, 2), nullable=False, default=0.00)
|
|
||||||
discount_amount = Column(Numeric(10, 2), nullable=False, default=0.00)
|
|
||||||
total_amount = Column(Numeric(10, 2), nullable=False)
|
total_amount = Column(Numeric(10, 2), nullable=False)
|
||||||
amount_paid = Column(Numeric(10, 2), nullable=False, default=0.00)
|
amount_paid = Column(Numeric(10, 2), nullable=False, default=0.0)
|
||||||
balance_due = Column(Numeric(10, 2), nullable=False)
|
balance_due = Column(Numeric(10, 2), nullable=False)
|
||||||
|
|
||||||
# Status
|
|
||||||
status = Column(Enum(InvoiceStatus), nullable=False, default=InvoiceStatus.draft)
|
status = Column(Enum(InvoiceStatus), nullable=False, default=InvoiceStatus.draft)
|
||||||
is_proforma = Column(Boolean, nullable=False, default=False) # True for proforma invoices
|
is_proforma = Column(Boolean, nullable=False, default=False)
|
||||||
|
|
||||||
# Company/Organization information (for admin to manage)
|
|
||||||
company_name = Column(String(200), nullable=True)
|
company_name = Column(String(200), nullable=True)
|
||||||
company_address = Column(Text, nullable=True)
|
company_address = Column(Text, nullable=True)
|
||||||
company_phone = Column(String(50), nullable=True)
|
company_phone = Column(String(50), nullable=True)
|
||||||
company_email = Column(String(100), nullable=True)
|
company_email = Column(String(100), nullable=True)
|
||||||
company_tax_id = Column(String(100), nullable=True)
|
company_tax_id = Column(String(100), nullable=True)
|
||||||
company_logo_url = Column(String(500), nullable=True)
|
company_logo_url = Column(String(500), nullable=True)
|
||||||
|
|
||||||
# Customer information (snapshot at invoice creation)
|
|
||||||
customer_name = Column(String(200), nullable=False)
|
customer_name = Column(String(200), nullable=False)
|
||||||
customer_email = Column(String(100), nullable=False)
|
customer_email = Column(String(100), nullable=False)
|
||||||
customer_address = Column(Text, nullable=True)
|
customer_address = Column(Text, nullable=True)
|
||||||
customer_phone = Column(String(50), nullable=True)
|
customer_phone = Column(String(50), nullable=True)
|
||||||
customer_tax_id = Column(String(100), nullable=True)
|
customer_tax_id = Column(String(100), nullable=True)
|
||||||
|
|
||||||
# Additional information
|
|
||||||
notes = Column(Text, nullable=True)
|
notes = Column(Text, nullable=True)
|
||||||
terms_and_conditions = Column(Text, nullable=True)
|
terms_and_conditions = Column(Text, nullable=True)
|
||||||
payment_instructions = Column(Text, nullable=True)
|
payment_instructions = Column(Text, nullable=True)
|
||||||
|
created_by_id = Column(Integer, ForeignKey('users.id'), nullable=True)
|
||||||
# Metadata
|
updated_by_id = Column(Integer, ForeignKey('users.id'), nullable=True)
|
||||||
created_by_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
|
||||||
updated_by_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||||
|
booking = relationship('Booking', back_populates='invoices')
|
||||||
# Relationships
|
user = relationship('User', foreign_keys=[user_id], backref='invoices')
|
||||||
booking = relationship("Booking", back_populates="invoices")
|
created_by = relationship('User', foreign_keys=[created_by_id])
|
||||||
user = relationship("User", foreign_keys=[user_id], backref="invoices")
|
updated_by = relationship('User', foreign_keys=[updated_by_id])
|
||||||
created_by = relationship("User", foreign_keys=[created_by_id])
|
items = relationship('InvoiceItem', back_populates='invoice', cascade='all, delete-orphan')
|
||||||
updated_by = relationship("User", foreign_keys=[updated_by_id])
|
|
||||||
items = relationship("InvoiceItem", back_populates="invoice", cascade="all, delete-orphan")
|
|
||||||
|
|
||||||
|
|
||||||
class InvoiceItem(Base):
|
class InvoiceItem(Base):
|
||||||
__tablename__ = "invoice_items"
|
__tablename__ = 'invoice_items'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
invoice_id = Column(Integer, ForeignKey("invoices.id"), nullable=False)
|
invoice_id = Column(Integer, ForeignKey('invoices.id'), nullable=False)
|
||||||
|
|
||||||
# Item details
|
|
||||||
description = Column(String(500), nullable=False)
|
description = Column(String(500), nullable=False)
|
||||||
quantity = Column(Numeric(10, 2), nullable=False, default=1.00)
|
quantity = Column(Numeric(10, 2), nullable=False, default=1.0)
|
||||||
unit_price = Column(Numeric(10, 2), nullable=False)
|
unit_price = Column(Numeric(10, 2), nullable=False)
|
||||||
tax_rate = Column(Numeric(5, 2), nullable=False, default=0.00)
|
tax_rate = Column(Numeric(5, 2), nullable=False, default=0.0)
|
||||||
discount_amount = Column(Numeric(10, 2), nullable=False, default=0.00)
|
discount_amount = Column(Numeric(10, 2), nullable=False, default=0.0)
|
||||||
line_total = Column(Numeric(10, 2), nullable=False)
|
line_total = Column(Numeric(10, 2), nullable=False)
|
||||||
|
room_id = Column(Integer, ForeignKey('rooms.id'), nullable=True)
|
||||||
# Optional reference to booking items
|
service_id = Column(Integer, ForeignKey('services.id'), nullable=True)
|
||||||
room_id = Column(Integer, ForeignKey("rooms.id"), nullable=True)
|
|
||||||
service_id = Column(Integer, ForeignKey("services.id"), nullable=True)
|
|
||||||
|
|
||||||
# Metadata
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||||
|
invoice = relationship('Invoice', back_populates='items')
|
||||||
# Relationships
|
room = relationship('Room')
|
||||||
invoice = relationship("Invoice", back_populates="items")
|
service = relationship('Service')
|
||||||
room = relationship("Room")
|
|
||||||
service = relationship("Service")
|
|
||||||
|
|
||||||
@@ -2,31 +2,23 @@ from sqlalchemy import Column, Integer, String, Text, DateTime, Boolean, Enum as
|
|||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import enum
|
import enum
|
||||||
|
|
||||||
from ..config.database import Base
|
from ..config.database import Base
|
||||||
|
|
||||||
|
|
||||||
class PageType(str, enum.Enum):
|
class PageType(str, enum.Enum):
|
||||||
HOME = "home"
|
HOME = 'home'
|
||||||
CONTACT = "contact"
|
CONTACT = 'contact'
|
||||||
ABOUT = "about"
|
ABOUT = 'about'
|
||||||
FOOTER = "footer"
|
FOOTER = 'footer'
|
||||||
SEO = "seo"
|
SEO = 'seo'
|
||||||
|
|
||||||
|
|
||||||
class PageContent(Base):
|
class PageContent(Base):
|
||||||
__tablename__ = "page_contents"
|
__tablename__ = 'page_contents'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
page_type = Column(SQLEnum(PageType), nullable=False, unique=True, index=True)
|
page_type = Column(SQLEnum(PageType), nullable=False, unique=True, index=True)
|
||||||
|
|
||||||
# General content fields
|
|
||||||
title = Column(String(500), nullable=True)
|
title = Column(String(500), nullable=True)
|
||||||
subtitle = Column(String(1000), nullable=True)
|
subtitle = Column(String(1000), nullable=True)
|
||||||
description = Column(Text, nullable=True)
|
description = Column(Text, nullable=True)
|
||||||
content = Column(Text, nullable=True) # Rich text content
|
content = Column(Text, nullable=True)
|
||||||
|
|
||||||
# SEO fields
|
|
||||||
meta_title = Column(String(500), nullable=True)
|
meta_title = Column(String(500), nullable=True)
|
||||||
meta_description = Column(Text, nullable=True)
|
meta_description = Column(Text, nullable=True)
|
||||||
meta_keywords = Column(String(1000), nullable=True)
|
meta_keywords = Column(String(1000), nullable=True)
|
||||||
@@ -34,67 +26,57 @@ class PageContent(Base):
|
|||||||
og_description = Column(Text, nullable=True)
|
og_description = Column(Text, nullable=True)
|
||||||
og_image = Column(String(1000), nullable=True)
|
og_image = Column(String(1000), nullable=True)
|
||||||
canonical_url = Column(String(1000), nullable=True)
|
canonical_url = Column(String(1000), nullable=True)
|
||||||
|
contact_info = Column(Text, nullable=True)
|
||||||
# Contact/Footer specific fields (stored as JSON strings)
|
map_url = Column(String(1000), nullable=True)
|
||||||
contact_info = Column(Text, nullable=True) # JSON: phone, email, address
|
social_links = Column(Text, nullable=True)
|
||||||
map_url = Column(String(1000), nullable=True) # Google Maps embed URL
|
footer_links = Column(Text, nullable=True)
|
||||||
social_links = Column(Text, nullable=True) # JSON: facebook, twitter, instagram, etc.
|
badges = Column(Text, nullable=True)
|
||||||
footer_links = Column(Text, nullable=True) # JSON: quick links, support links
|
copyright_text = Column(Text, nullable=True)
|
||||||
badges = Column(Text, nullable=True) # JSON: array of badges with text and icon
|
|
||||||
copyright_text = Column(Text, nullable=True) # Copyright text with {YEAR} placeholder for automatic year
|
|
||||||
|
|
||||||
# Home page specific
|
|
||||||
hero_title = Column(String(500), nullable=True)
|
hero_title = Column(String(500), nullable=True)
|
||||||
hero_subtitle = Column(String(1000), nullable=True)
|
hero_subtitle = Column(String(1000), nullable=True)
|
||||||
hero_image = Column(String(1000), nullable=True)
|
hero_image = Column(String(1000), nullable=True)
|
||||||
|
|
||||||
# About page specific
|
|
||||||
story_content = Column(Text, nullable=True)
|
story_content = Column(Text, nullable=True)
|
||||||
values = Column(Text, nullable=True) # JSON array of values
|
values = Column(Text, nullable=True)
|
||||||
features = Column(Text, nullable=True) # JSON array of features
|
features = Column(Text, nullable=True)
|
||||||
about_hero_image = Column(Text, nullable=True) # Hero image for about page
|
about_hero_image = Column(Text, nullable=True)
|
||||||
mission = Column(Text, nullable=True) # Mission statement
|
mission = Column(Text, nullable=True)
|
||||||
vision = Column(Text, nullable=True) # Vision statement
|
vision = Column(Text, nullable=True)
|
||||||
team = Column(Text, nullable=True) # JSON array of team members with name, role, image, bio, social_links
|
team = Column(Text, nullable=True)
|
||||||
timeline = Column(Text, nullable=True) # JSON array of timeline events with year, title, description, image
|
timeline = Column(Text, nullable=True)
|
||||||
achievements = Column(Text, nullable=True) # JSON array of achievements with icon, title, description, year, image
|
achievements = Column(Text, nullable=True)
|
||||||
|
|
||||||
# Home page luxury sections
|
|
||||||
luxury_section_title = Column(Text, nullable=True)
|
luxury_section_title = Column(Text, nullable=True)
|
||||||
luxury_section_subtitle = Column(Text, nullable=True)
|
luxury_section_subtitle = Column(Text, nullable=True)
|
||||||
luxury_section_image = Column(Text, nullable=True)
|
luxury_section_image = Column(Text, nullable=True)
|
||||||
luxury_features = Column(Text, nullable=True) # JSON array of features with icon, title, description
|
luxury_features = Column(Text, nullable=True)
|
||||||
luxury_gallery_section_title = Column(Text, nullable=True)
|
luxury_gallery_section_title = Column(Text, nullable=True)
|
||||||
luxury_gallery_section_subtitle = Column(Text, nullable=True)
|
luxury_gallery_section_subtitle = Column(Text, nullable=True)
|
||||||
luxury_gallery = Column(Text, nullable=True) # JSON array of image URLs
|
luxury_gallery = Column(Text, nullable=True)
|
||||||
luxury_testimonials_section_title = Column(Text, nullable=True)
|
luxury_testimonials_section_title = Column(Text, nullable=True)
|
||||||
luxury_testimonials_section_subtitle = Column(Text, nullable=True)
|
luxury_testimonials_section_subtitle = Column(Text, nullable=True)
|
||||||
luxury_testimonials = Column(Text, nullable=True) # JSON array of testimonials
|
luxury_testimonials = Column(Text, nullable=True)
|
||||||
amenities_section_title = Column(String(500), nullable=True)
|
amenities_section_title = Column(String(500), nullable=True)
|
||||||
amenities_section_subtitle = Column(String(1000), nullable=True)
|
amenities_section_subtitle = Column(String(1000), nullable=True)
|
||||||
amenities = Column(Text, nullable=True) # JSON array of amenities with icon, title, description, image
|
amenities = Column(Text, nullable=True)
|
||||||
testimonials_section_title = Column(String(500), nullable=True)
|
testimonials_section_title = Column(String(500), nullable=True)
|
||||||
testimonials_section_subtitle = Column(String(1000), nullable=True)
|
testimonials_section_subtitle = Column(String(1000), nullable=True)
|
||||||
testimonials = Column(Text, nullable=True) # JSON array of testimonials with name, role, image, rating, comment
|
testimonials = Column(Text, nullable=True)
|
||||||
gallery_section_title = Column(String(500), nullable=True)
|
gallery_section_title = Column(String(500), nullable=True)
|
||||||
gallery_section_subtitle = Column(String(1000), nullable=True)
|
gallery_section_subtitle = Column(String(1000), nullable=True)
|
||||||
gallery_images = Column(Text, nullable=True) # JSON array of image URLs
|
gallery_images = Column(Text, nullable=True)
|
||||||
about_preview_title = Column(String(500), nullable=True)
|
about_preview_title = Column(String(500), nullable=True)
|
||||||
about_preview_subtitle = Column(String(1000), nullable=True)
|
about_preview_subtitle = Column(String(1000), nullable=True)
|
||||||
about_preview_content = Column(Text, nullable=True)
|
about_preview_content = Column(Text, nullable=True)
|
||||||
about_preview_image = Column(String(1000), nullable=True)
|
about_preview_image = Column(String(1000), nullable=True)
|
||||||
stats = Column(Text, nullable=True) # JSON array of stats with number, label, icon
|
stats = Column(Text, nullable=True)
|
||||||
|
|
||||||
# Additional luxury sections
|
|
||||||
luxury_services_section_title = Column(Text, nullable=True)
|
luxury_services_section_title = Column(Text, nullable=True)
|
||||||
luxury_services_section_subtitle = Column(Text, nullable=True)
|
luxury_services_section_subtitle = Column(Text, nullable=True)
|
||||||
luxury_services = Column(Text, nullable=True) # JSON array of services with icon, title, description, image
|
luxury_services = Column(Text, nullable=True)
|
||||||
luxury_experiences_section_title = Column(Text, nullable=True)
|
luxury_experiences_section_title = Column(Text, nullable=True)
|
||||||
luxury_experiences_section_subtitle = Column(Text, nullable=True)
|
luxury_experiences_section_subtitle = Column(Text, nullable=True)
|
||||||
luxury_experiences = Column(Text, nullable=True) # JSON array of experiences with icon, title, description, image
|
luxury_experiences = Column(Text, nullable=True)
|
||||||
awards_section_title = Column(Text, nullable=True)
|
awards_section_title = Column(Text, nullable=True)
|
||||||
awards_section_subtitle = Column(Text, nullable=True)
|
awards_section_subtitle = Column(Text, nullable=True)
|
||||||
awards = Column(Text, nullable=True) # JSON array of awards with icon, title, description, image, year
|
awards = Column(Text, nullable=True)
|
||||||
cta_title = Column(Text, nullable=True)
|
cta_title = Column(Text, nullable=True)
|
||||||
cta_subtitle = Column(Text, nullable=True)
|
cta_subtitle = Column(Text, nullable=True)
|
||||||
cta_button_text = Column(Text, nullable=True)
|
cta_button_text = Column(Text, nullable=True)
|
||||||
@@ -102,12 +84,7 @@ class PageContent(Base):
|
|||||||
cta_image = Column(Text, nullable=True)
|
cta_image = Column(Text, nullable=True)
|
||||||
partners_section_title = Column(Text, nullable=True)
|
partners_section_title = Column(Text, nullable=True)
|
||||||
partners_section_subtitle = Column(Text, nullable=True)
|
partners_section_subtitle = Column(Text, nullable=True)
|
||||||
partners = Column(Text, nullable=True) # JSON array of partners with name, logo, link
|
partners = Column(Text, nullable=True)
|
||||||
|
|
||||||
# Status
|
|
||||||
is_active = Column(Boolean, default=True, nullable=False)
|
is_active = Column(Boolean, default=True, nullable=False)
|
||||||
|
|
||||||
# Timestamps
|
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||||
|
|
||||||
@@ -3,18 +3,13 @@ from sqlalchemy.orm import relationship
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from ..config.database import Base
|
from ..config.database import Base
|
||||||
|
|
||||||
|
|
||||||
class PasswordResetToken(Base):
|
class PasswordResetToken(Base):
|
||||||
__tablename__ = "password_reset_tokens"
|
__tablename__ = 'password_reset_tokens'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
|
||||||
token = Column(String(255), unique=True, nullable=False, index=True)
|
token = Column(String(255), unique=True, nullable=False, index=True)
|
||||||
expires_at = Column(DateTime, nullable=False)
|
expires_at = Column(DateTime, nullable=False)
|
||||||
used = Column(Boolean, default=False, nullable=False)
|
used = Column(Boolean, default=False, nullable=False)
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||||
|
user = relationship('User')
|
||||||
# Relationships
|
|
||||||
user = relationship("User")
|
|
||||||
|
|
||||||
@@ -4,48 +4,40 @@ from datetime import datetime
|
|||||||
import enum
|
import enum
|
||||||
from ..config.database import Base
|
from ..config.database import Base
|
||||||
|
|
||||||
|
|
||||||
class PaymentMethod(str, enum.Enum):
|
class PaymentMethod(str, enum.Enum):
|
||||||
cash = "cash"
|
cash = 'cash'
|
||||||
credit_card = "credit_card"
|
credit_card = 'credit_card'
|
||||||
debit_card = "debit_card"
|
debit_card = 'debit_card'
|
||||||
bank_transfer = "bank_transfer"
|
bank_transfer = 'bank_transfer'
|
||||||
e_wallet = "e_wallet"
|
e_wallet = 'e_wallet'
|
||||||
stripe = "stripe"
|
stripe = 'stripe'
|
||||||
paypal = "paypal"
|
paypal = 'paypal'
|
||||||
|
|
||||||
|
|
||||||
class PaymentType(str, enum.Enum):
|
class PaymentType(str, enum.Enum):
|
||||||
full = "full"
|
full = 'full'
|
||||||
deposit = "deposit"
|
deposit = 'deposit'
|
||||||
remaining = "remaining"
|
remaining = 'remaining'
|
||||||
|
|
||||||
|
|
||||||
class PaymentStatus(str, enum.Enum):
|
class PaymentStatus(str, enum.Enum):
|
||||||
pending = "pending"
|
pending = 'pending'
|
||||||
completed = "completed"
|
completed = 'completed'
|
||||||
failed = "failed"
|
failed = 'failed'
|
||||||
refunded = "refunded"
|
refunded = 'refunded'
|
||||||
|
|
||||||
|
|
||||||
class Payment(Base):
|
class Payment(Base):
|
||||||
__tablename__ = "payments"
|
__tablename__ = 'payments'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
booking_id = Column(Integer, ForeignKey("bookings.id"), nullable=False)
|
booking_id = Column(Integer, ForeignKey('bookings.id'), nullable=False)
|
||||||
amount = Column(Numeric(10, 2), nullable=False)
|
amount = Column(Numeric(10, 2), nullable=False)
|
||||||
payment_method = Column(Enum(PaymentMethod), nullable=False)
|
payment_method = Column(Enum(PaymentMethod), nullable=False)
|
||||||
payment_type = Column(Enum(PaymentType), nullable=False, default=PaymentType.full)
|
payment_type = Column(Enum(PaymentType), nullable=False, default=PaymentType.full)
|
||||||
deposit_percentage = Column(Integer, nullable=True)
|
deposit_percentage = Column(Integer, nullable=True)
|
||||||
related_payment_id = Column(Integer, ForeignKey("payments.id"), nullable=True)
|
related_payment_id = Column(Integer, ForeignKey('payments.id'), nullable=True)
|
||||||
payment_status = Column(Enum(PaymentStatus), nullable=False, default=PaymentStatus.pending)
|
payment_status = Column(Enum(PaymentStatus), nullable=False, default=PaymentStatus.pending)
|
||||||
transaction_id = Column(String(100), nullable=True)
|
transaction_id = Column(String(100), nullable=True)
|
||||||
payment_date = Column(DateTime, nullable=True)
|
payment_date = Column(DateTime, nullable=True)
|
||||||
notes = Column(Text, nullable=True)
|
notes = Column(Text, nullable=True)
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||||
|
booking = relationship('Booking', back_populates='payments')
|
||||||
# Relationships
|
related_payment = relationship('Payment', remote_side=[id], backref='related_payments')
|
||||||
booking = relationship("Booking", back_populates="payments")
|
|
||||||
related_payment = relationship("Payment", remote_side=[id], backref="related_payments")
|
|
||||||
|
|
||||||
@@ -4,15 +4,12 @@ from datetime import datetime
|
|||||||
import enum
|
import enum
|
||||||
from ..config.database import Base
|
from ..config.database import Base
|
||||||
|
|
||||||
|
|
||||||
class DiscountType(str, enum.Enum):
|
class DiscountType(str, enum.Enum):
|
||||||
percentage = "percentage"
|
percentage = 'percentage'
|
||||||
fixed_amount = "fixed_amount"
|
fixed_amount = 'fixed_amount'
|
||||||
|
|
||||||
|
|
||||||
class Promotion(Base):
|
class Promotion(Base):
|
||||||
__tablename__ = "promotions"
|
__tablename__ = 'promotions'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
code = Column(String(50), unique=True, nullable=False, index=True)
|
code = Column(String(50), unique=True, nullable=False, index=True)
|
||||||
name = Column(String(100), nullable=False)
|
name = Column(String(100), nullable=False)
|
||||||
@@ -43,18 +40,13 @@ class Promotion(Base):
|
|||||||
def calculate_discount(self, booking_amount):
|
def calculate_discount(self, booking_amount):
|
||||||
if not self.is_valid():
|
if not self.is_valid():
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
if self.min_booking_amount and booking_amount < float(self.min_booking_amount):
|
if self.min_booking_amount and booking_amount < float(self.min_booking_amount):
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
discount = 0.0
|
discount = 0.0
|
||||||
if self.discount_type == DiscountType.percentage:
|
if self.discount_type == DiscountType.percentage:
|
||||||
discount = float(booking_amount) * float(self.discount_value) / 100.0
|
discount = float(booking_amount) * float(self.discount_value) / 100.0
|
||||||
elif self.discount_type == DiscountType.fixed_amount:
|
elif self.discount_type == DiscountType.fixed_amount:
|
||||||
discount = float(self.discount_value)
|
discount = float(self.discount_value)
|
||||||
|
|
||||||
if self.max_discount_amount and discount > float(self.max_discount_amount):
|
if self.max_discount_amount and discount > float(self.max_discount_amount):
|
||||||
discount = float(self.max_discount_amount)
|
discount = float(self.max_discount_amount)
|
||||||
|
return discount
|
||||||
return discount
|
|
||||||
|
|
||||||
@@ -3,16 +3,11 @@ from sqlalchemy.orm import relationship
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from ..config.database import Base
|
from ..config.database import Base
|
||||||
|
|
||||||
|
|
||||||
class RefreshToken(Base):
|
class RefreshToken(Base):
|
||||||
__tablename__ = "refresh_tokens"
|
__tablename__ = 'refresh_tokens'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
|
||||||
token = Column(String(500), unique=True, nullable=False, index=True)
|
token = Column(String(500), unique=True, nullable=False, index=True)
|
||||||
expires_at = Column(DateTime, nullable=False)
|
expires_at = Column(DateTime, nullable=False)
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||||
|
user = relationship('User', back_populates='refresh_tokens')
|
||||||
# Relationships
|
|
||||||
user = relationship("User", back_populates="refresh_tokens")
|
|
||||||
|
|
||||||
@@ -4,26 +4,20 @@ from datetime import datetime
|
|||||||
import enum
|
import enum
|
||||||
from ..config.database import Base
|
from ..config.database import Base
|
||||||
|
|
||||||
|
|
||||||
class ReviewStatus(str, enum.Enum):
|
class ReviewStatus(str, enum.Enum):
|
||||||
pending = "pending"
|
pending = 'pending'
|
||||||
approved = "approved"
|
approved = 'approved'
|
||||||
rejected = "rejected"
|
rejected = 'rejected'
|
||||||
|
|
||||||
|
|
||||||
class Review(Base):
|
class Review(Base):
|
||||||
__tablename__ = "reviews"
|
__tablename__ = 'reviews'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
|
||||||
room_id = Column(Integer, ForeignKey("rooms.id"), nullable=False)
|
room_id = Column(Integer, ForeignKey('rooms.id'), nullable=False)
|
||||||
rating = Column(Integer, nullable=False)
|
rating = Column(Integer, nullable=False)
|
||||||
comment = Column(Text, nullable=False)
|
comment = Column(Text, nullable=False)
|
||||||
status = Column(Enum(ReviewStatus), nullable=False, default=ReviewStatus.pending)
|
status = Column(Enum(ReviewStatus), nullable=False, default=ReviewStatus.pending)
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||||
|
user = relationship('User', back_populates='reviews')
|
||||||
# Relationships
|
room = relationship('Room', back_populates='reviews')
|
||||||
user = relationship("User", back_populates="reviews")
|
|
||||||
room = relationship("Room", back_populates="reviews")
|
|
||||||
|
|
||||||
@@ -3,16 +3,11 @@ from sqlalchemy.orm import relationship
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from ..config.database import Base
|
from ..config.database import Base
|
||||||
|
|
||||||
|
|
||||||
class Role(Base):
|
class Role(Base):
|
||||||
__tablename__ = "roles"
|
__tablename__ = 'roles'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
name = Column(String(50), unique=True, nullable=False, index=True)
|
name = Column(String(50), unique=True, nullable=False, index=True)
|
||||||
description = Column(String(255), nullable=True)
|
description = Column(String(255), nullable=True)
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||||
|
users = relationship('User', back_populates='role')
|
||||||
# Relationships
|
|
||||||
users = relationship("User", back_populates="role")
|
|
||||||
|
|
||||||
@@ -4,36 +4,30 @@ from datetime import datetime
|
|||||||
import enum
|
import enum
|
||||||
from ..config.database import Base
|
from ..config.database import Base
|
||||||
|
|
||||||
|
|
||||||
class RoomStatus(str, enum.Enum):
|
class RoomStatus(str, enum.Enum):
|
||||||
available = "available"
|
available = 'available'
|
||||||
occupied = "occupied"
|
occupied = 'occupied'
|
||||||
maintenance = "maintenance"
|
maintenance = 'maintenance'
|
||||||
cleaning = "cleaning"
|
cleaning = 'cleaning'
|
||||||
|
|
||||||
|
|
||||||
class Room(Base):
|
class Room(Base):
|
||||||
__tablename__ = "rooms"
|
__tablename__ = 'rooms'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
room_type_id = Column(Integer, ForeignKey("room_types.id"), nullable=False)
|
room_type_id = Column(Integer, ForeignKey('room_types.id'), nullable=False)
|
||||||
room_number = Column(String(20), unique=True, nullable=False, index=True)
|
room_number = Column(String(20), unique=True, nullable=False, index=True)
|
||||||
floor = Column(Integer, nullable=False)
|
floor = Column(Integer, nullable=False)
|
||||||
status = Column(Enum(RoomStatus), nullable=False, default=RoomStatus.available)
|
status = Column(Enum(RoomStatus), nullable=False, default=RoomStatus.available)
|
||||||
price = Column(Numeric(10, 2), nullable=False)
|
price = Column(Numeric(10, 2), nullable=False)
|
||||||
featured = Column(Boolean, nullable=False, default=False)
|
featured = Column(Boolean, nullable=False, default=False)
|
||||||
capacity = Column(Integer, nullable=True) # Room-specific capacity, overrides room_type capacity
|
capacity = Column(Integer, nullable=True)
|
||||||
room_size = Column(String(50), nullable=True) # e.g., "1 Room", "2 Rooms", "50 sqm"
|
room_size = Column(String(50), nullable=True)
|
||||||
view = Column(String(100), nullable=True) # e.g., "City View", "Ocean View", etc.
|
view = Column(String(100), nullable=True)
|
||||||
images = Column(JSON, nullable=True)
|
images = Column(JSON, nullable=True)
|
||||||
amenities = Column(JSON, nullable=True)
|
amenities = Column(JSON, nullable=True)
|
||||||
description = Column(Text, nullable=True)
|
description = Column(Text, nullable=True)
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||||
|
room_type = relationship('RoomType', back_populates='rooms')
|
||||||
# Relationships
|
bookings = relationship('Booking', back_populates='room')
|
||||||
room_type = relationship("RoomType", back_populates="rooms")
|
reviews = relationship('Review', back_populates='room')
|
||||||
bookings = relationship("Booking", back_populates="room")
|
favorites = relationship('Favorite', back_populates='room', cascade='all, delete-orphan')
|
||||||
reviews = relationship("Review", back_populates="room")
|
|
||||||
favorites = relationship("Favorite", back_populates="room", cascade="all, delete-orphan")
|
|
||||||
|
|
||||||
@@ -3,10 +3,8 @@ from sqlalchemy.orm import relationship
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from ..config.database import Base
|
from ..config.database import Base
|
||||||
|
|
||||||
|
|
||||||
class RoomType(Base):
|
class RoomType(Base):
|
||||||
__tablename__ = "room_types"
|
__tablename__ = 'room_types'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
name = Column(String(100), unique=True, nullable=False)
|
name = Column(String(100), unique=True, nullable=False)
|
||||||
description = Column(Text, nullable=True)
|
description = Column(Text, nullable=True)
|
||||||
@@ -15,7 +13,4 @@ class RoomType(Base):
|
|||||||
amenities = Column(JSON, nullable=True)
|
amenities = Column(JSON, nullable=True)
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||||
|
rooms = relationship('Room', back_populates='room_type')
|
||||||
# Relationships
|
|
||||||
rooms = relationship("Room", back_populates="room_type")
|
|
||||||
|
|
||||||
@@ -3,10 +3,8 @@ from sqlalchemy.orm import relationship
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from ..config.database import Base
|
from ..config.database import Base
|
||||||
|
|
||||||
|
|
||||||
class Service(Base):
|
class Service(Base):
|
||||||
__tablename__ = "services"
|
__tablename__ = 'services'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
name = Column(String(100), nullable=False)
|
name = Column(String(100), nullable=False)
|
||||||
description = Column(Text, nullable=True)
|
description = Column(Text, nullable=True)
|
||||||
@@ -15,7 +13,4 @@ class Service(Base):
|
|||||||
is_active = Column(Boolean, nullable=False, default=True)
|
is_active = Column(Boolean, nullable=False, default=True)
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||||
|
service_usages = relationship('ServiceUsage', back_populates='service')
|
||||||
# Relationships
|
|
||||||
service_usages = relationship("ServiceUsage", back_populates="service")
|
|
||||||
|
|
||||||
@@ -4,66 +4,53 @@ from datetime import datetime
|
|||||||
import enum
|
import enum
|
||||||
from ..config.database import Base
|
from ..config.database import Base
|
||||||
|
|
||||||
|
|
||||||
class ServiceBookingStatus(str, enum.Enum):
|
class ServiceBookingStatus(str, enum.Enum):
|
||||||
pending = "pending"
|
pending = 'pending'
|
||||||
confirmed = "confirmed"
|
confirmed = 'confirmed'
|
||||||
completed = "completed"
|
completed = 'completed'
|
||||||
cancelled = "cancelled"
|
cancelled = 'cancelled'
|
||||||
|
|
||||||
|
|
||||||
class ServiceBooking(Base):
|
class ServiceBooking(Base):
|
||||||
__tablename__ = "service_bookings"
|
__tablename__ = 'service_bookings'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
booking_number = Column(String(50), unique=True, nullable=False, index=True)
|
booking_number = Column(String(50), unique=True, nullable=False, index=True)
|
||||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
user_id = Column(Integer, ForeignKey('users.id'), nullable=False)
|
||||||
total_amount = Column(Numeric(10, 2), nullable=False)
|
total_amount = Column(Numeric(10, 2), nullable=False)
|
||||||
status = Column(Enum(ServiceBookingStatus), nullable=False, default=ServiceBookingStatus.pending)
|
status = Column(Enum(ServiceBookingStatus), nullable=False, default=ServiceBookingStatus.pending)
|
||||||
notes = Column(Text, nullable=True)
|
notes = Column(Text, nullable=True)
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||||
|
user = relationship('User', back_populates='service_bookings')
|
||||||
# Relationships
|
service_items = relationship('ServiceBookingItem', back_populates='service_booking', cascade='all, delete-orphan')
|
||||||
user = relationship("User", back_populates="service_bookings")
|
payments = relationship('ServicePayment', back_populates='service_booking', cascade='all, delete-orphan')
|
||||||
service_items = relationship("ServiceBookingItem", back_populates="service_booking", cascade="all, delete-orphan")
|
|
||||||
payments = relationship("ServicePayment", back_populates="service_booking", cascade="all, delete-orphan")
|
|
||||||
|
|
||||||
|
|
||||||
class ServiceBookingItem(Base):
|
class ServiceBookingItem(Base):
|
||||||
__tablename__ = "service_booking_items"
|
__tablename__ = 'service_booking_items'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
service_booking_id = Column(Integer, ForeignKey("service_bookings.id"), nullable=False)
|
service_booking_id = Column(Integer, ForeignKey('service_bookings.id'), nullable=False)
|
||||||
service_id = Column(Integer, ForeignKey("services.id"), nullable=False)
|
service_id = Column(Integer, ForeignKey('services.id'), nullable=False)
|
||||||
quantity = Column(Integer, nullable=False, default=1)
|
quantity = Column(Integer, nullable=False, default=1)
|
||||||
unit_price = Column(Numeric(10, 2), nullable=False)
|
unit_price = Column(Numeric(10, 2), nullable=False)
|
||||||
total_price = Column(Numeric(10, 2), nullable=False)
|
total_price = Column(Numeric(10, 2), nullable=False)
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||||
|
service_booking = relationship('ServiceBooking', back_populates='service_items')
|
||||||
# Relationships
|
service = relationship('Service')
|
||||||
service_booking = relationship("ServiceBooking", back_populates="service_items")
|
|
||||||
service = relationship("Service")
|
|
||||||
|
|
||||||
|
|
||||||
class ServicePaymentStatus(str, enum.Enum):
|
class ServicePaymentStatus(str, enum.Enum):
|
||||||
pending = "pending"
|
pending = 'pending'
|
||||||
completed = "completed"
|
completed = 'completed'
|
||||||
failed = "failed"
|
failed = 'failed'
|
||||||
refunded = "refunded"
|
refunded = 'refunded'
|
||||||
|
|
||||||
|
|
||||||
class ServicePaymentMethod(str, enum.Enum):
|
class ServicePaymentMethod(str, enum.Enum):
|
||||||
cash = "cash"
|
cash = 'cash'
|
||||||
stripe = "stripe"
|
stripe = 'stripe'
|
||||||
bank_transfer = "bank_transfer"
|
bank_transfer = 'bank_transfer'
|
||||||
|
|
||||||
|
|
||||||
class ServicePayment(Base):
|
class ServicePayment(Base):
|
||||||
__tablename__ = "service_payments"
|
__tablename__ = 'service_payments'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
service_booking_id = Column(Integer, ForeignKey("service_bookings.id"), nullable=False)
|
service_booking_id = Column(Integer, ForeignKey('service_bookings.id'), nullable=False)
|
||||||
amount = Column(Numeric(10, 2), nullable=False)
|
amount = Column(Numeric(10, 2), nullable=False)
|
||||||
payment_method = Column(Enum(ServicePaymentMethod), nullable=False)
|
payment_method = Column(Enum(ServicePaymentMethod), nullable=False)
|
||||||
payment_status = Column(Enum(ServicePaymentStatus), nullable=False, default=ServicePaymentStatus.pending)
|
payment_status = Column(Enum(ServicePaymentStatus), nullable=False, default=ServicePaymentStatus.pending)
|
||||||
@@ -72,7 +59,4 @@ class ServicePayment(Base):
|
|||||||
notes = Column(Text, nullable=True)
|
notes = Column(Text, nullable=True)
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||||
|
service_booking = relationship('ServiceBooking', back_populates='payments')
|
||||||
# Relationships
|
|
||||||
service_booking = relationship("ServiceBooking", back_populates="payments")
|
|
||||||
|
|
||||||
@@ -3,13 +3,11 @@ from sqlalchemy.orm import relationship
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from ..config.database import Base
|
from ..config.database import Base
|
||||||
|
|
||||||
|
|
||||||
class ServiceUsage(Base):
|
class ServiceUsage(Base):
|
||||||
__tablename__ = "service_usages"
|
__tablename__ = 'service_usages'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
booking_id = Column(Integer, ForeignKey("bookings.id"), nullable=False)
|
booking_id = Column(Integer, ForeignKey('bookings.id'), nullable=False)
|
||||||
service_id = Column(Integer, ForeignKey("services.id"), nullable=False)
|
service_id = Column(Integer, ForeignKey('services.id'), nullable=False)
|
||||||
quantity = Column(Integer, nullable=False, default=1)
|
quantity = Column(Integer, nullable=False, default=1)
|
||||||
unit_price = Column(Numeric(10, 2), nullable=False)
|
unit_price = Column(Numeric(10, 2), nullable=False)
|
||||||
total_price = Column(Numeric(10, 2), nullable=False)
|
total_price = Column(Numeric(10, 2), nullable=False)
|
||||||
@@ -17,8 +15,5 @@ class ServiceUsage(Base):
|
|||||||
notes = Column(Text, nullable=True)
|
notes = Column(Text, nullable=True)
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||||
|
booking = relationship('Booking', back_populates='service_usages')
|
||||||
# Relationships
|
service = relationship('Service', back_populates='service_usages')
|
||||||
booking = relationship("Booking", back_populates="service_usages")
|
|
||||||
service = relationship("Service", back_populates="service_usages")
|
|
||||||
|
|
||||||
@@ -3,19 +3,12 @@ from sqlalchemy.orm import relationship
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from ..config.database import Base
|
from ..config.database import Base
|
||||||
|
|
||||||
|
|
||||||
class SystemSettings(Base):
|
class SystemSettings(Base):
|
||||||
"""
|
__tablename__ = 'system_settings'
|
||||||
System-wide settings controlled by administrators.
|
|
||||||
Stores key-value pairs for platform configuration like currency, etc.
|
|
||||||
"""
|
|
||||||
__tablename__ = "system_settings"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
key = Column(String(100), unique=True, nullable=False, index=True)
|
key = Column(String(100), unique=True, nullable=False, index=True)
|
||||||
value = Column(Text, nullable=False)
|
value = Column(Text, nullable=False)
|
||||||
description = Column(Text, nullable=True)
|
description = Column(Text, nullable=True)
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||||
updated_by_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
updated_by_id = Column(Integer, ForeignKey('users.id'), nullable=True)
|
||||||
updated_by = relationship("User", lazy="joined")
|
updated_by = relationship('User', lazy='joined')
|
||||||
|
|
||||||
@@ -3,33 +3,30 @@ from sqlalchemy.orm import relationship
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from ..config.database import Base
|
from ..config.database import Base
|
||||||
|
|
||||||
|
|
||||||
class User(Base):
|
class User(Base):
|
||||||
__tablename__ = "users"
|
__tablename__ = 'users'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
|
||||||
role_id = Column(Integer, ForeignKey("roles.id"), nullable=False)
|
role_id = Column(Integer, ForeignKey('roles.id'), nullable=False)
|
||||||
email = Column(String(100), unique=True, nullable=False, index=True)
|
email = Column(String(100), unique=True, nullable=False, index=True)
|
||||||
password = Column(String(255), nullable=False)
|
password = Column(String(255), nullable=False)
|
||||||
full_name = Column(String(100), nullable=False)
|
full_name = Column(String(100), nullable=False)
|
||||||
phone = Column(String(20), nullable=True)
|
phone = Column(String(20), nullable=True)
|
||||||
address = Column(Text, nullable=True)
|
address = Column(Text, nullable=True)
|
||||||
avatar = Column(String(255), nullable=True)
|
avatar = Column(String(255), nullable=True)
|
||||||
currency = Column(String(3), nullable=False, default='VND') # ISO 4217 currency code
|
currency = Column(String(3), nullable=False, default='VND')
|
||||||
is_active = Column(Boolean, nullable=False, default=True)
|
is_active = Column(Boolean, nullable=False, default=True)
|
||||||
mfa_enabled = Column(Boolean, nullable=False, default=False)
|
mfa_enabled = Column(Boolean, nullable=False, default=False)
|
||||||
mfa_secret = Column(String(255), nullable=True) # TOTP secret key (encrypted in production)
|
mfa_secret = Column(String(255), nullable=True)
|
||||||
mfa_backup_codes = Column(Text, nullable=True) # JSON array of backup codes (hashed)
|
mfa_backup_codes = Column(Text, nullable=True)
|
||||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||||
|
role = relationship('Role', back_populates='users')
|
||||||
# Relationships
|
bookings = relationship('Booking', back_populates='user')
|
||||||
role = relationship("Role", back_populates="users")
|
refresh_tokens = relationship('RefreshToken', back_populates='user', cascade='all, delete-orphan')
|
||||||
bookings = relationship("Booking", back_populates="user")
|
checkins_processed = relationship('CheckInCheckOut', foreign_keys='CheckInCheckOut.checkin_by', back_populates='checked_in_by')
|
||||||
refresh_tokens = relationship("RefreshToken", back_populates="user", cascade="all, delete-orphan")
|
checkouts_processed = relationship('CheckInCheckOut', foreign_keys='CheckInCheckOut.checkout_by', back_populates='checked_out_by')
|
||||||
checkins_processed = relationship("CheckInCheckOut", foreign_keys="CheckInCheckOut.checkin_by", back_populates="checked_in_by")
|
reviews = relationship('Review', back_populates='user')
|
||||||
checkouts_processed = relationship("CheckInCheckOut", foreign_keys="CheckInCheckOut.checkout_by", back_populates="checked_out_by")
|
favorites = relationship('Favorite', back_populates='user', cascade='all, delete-orphan')
|
||||||
reviews = relationship("Review", back_populates="user")
|
service_bookings = relationship('ServiceBooking', back_populates='user')
|
||||||
favorites = relationship("Favorite", back_populates="user", cascade="all, delete-orphan")
|
visitor_chats = relationship('Chat', foreign_keys='Chat.visitor_id', back_populates='visitor')
|
||||||
service_bookings = relationship("ServiceBooking", back_populates="user")
|
staff_chats = relationship('Chat', foreign_keys='Chat.staff_id', back_populates='staff')
|
||||||
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
# Routes package
|
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
BIN
Backend/src/routes/__pycache__/chat_routes.cpython-312.pyc
Normal file
BIN
Backend/src/routes/__pycache__/chat_routes.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,75 +1,23 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from ..config.database import get_db
|
from ..config.database import get_db
|
||||||
from ..config.logging_config import get_logger
|
from ..config.logging_config import get_logger
|
||||||
from ..models.page_content import PageContent, PageType
|
from ..models.page_content import PageContent, PageType
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
router = APIRouter(prefix='/about', tags=['about'])
|
||||||
router = APIRouter(prefix="/about", tags=["about"])
|
|
||||||
|
|
||||||
|
|
||||||
def serialize_page_content(content: PageContent) -> dict:
|
def serialize_page_content(content: PageContent) -> dict:
|
||||||
"""Serialize PageContent model to dictionary"""
|
return {'id': content.id, 'page_type': content.page_type.value, 'title': content.title, 'subtitle': content.subtitle, 'description': content.description, 'content': content.content, 'meta_title': content.meta_title, 'meta_description': content.meta_description, 'meta_keywords': content.meta_keywords, 'og_title': content.og_title, 'og_description': content.og_description, 'og_image': content.og_image, 'canonical_url': content.canonical_url, 'story_content': content.story_content, 'values': json.loads(content.values) if content.values else None, 'features': json.loads(content.features) if content.features else None, 'about_hero_image': content.about_hero_image, 'mission': content.mission, 'vision': content.vision, 'team': json.loads(content.team) if content.team else None, 'timeline': json.loads(content.timeline) if content.timeline else None, 'achievements': json.loads(content.achievements) if content.achievements else None, 'is_active': content.is_active, 'created_at': content.created_at.isoformat() if content.created_at else None, 'updated_at': content.updated_at.isoformat() if content.updated_at else None}
|
||||||
return {
|
|
||||||
"id": content.id,
|
|
||||||
"page_type": content.page_type.value,
|
|
||||||
"title": content.title,
|
|
||||||
"subtitle": content.subtitle,
|
|
||||||
"description": content.description,
|
|
||||||
"content": content.content,
|
|
||||||
"meta_title": content.meta_title,
|
|
||||||
"meta_description": content.meta_description,
|
|
||||||
"meta_keywords": content.meta_keywords,
|
|
||||||
"og_title": content.og_title,
|
|
||||||
"og_description": content.og_description,
|
|
||||||
"og_image": content.og_image,
|
|
||||||
"canonical_url": content.canonical_url,
|
|
||||||
"story_content": content.story_content,
|
|
||||||
"values": json.loads(content.values) if content.values else None,
|
|
||||||
"features": json.loads(content.features) if content.features else None,
|
|
||||||
"about_hero_image": content.about_hero_image,
|
|
||||||
"mission": content.mission,
|
|
||||||
"vision": content.vision,
|
|
||||||
"team": json.loads(content.team) if content.team else None,
|
|
||||||
"timeline": json.loads(content.timeline) if content.timeline else None,
|
|
||||||
"achievements": json.loads(content.achievements) if content.achievements else None,
|
|
||||||
"is_active": content.is_active,
|
|
||||||
"created_at": content.created_at.isoformat() if content.created_at else None,
|
|
||||||
"updated_at": content.updated_at.isoformat() if content.updated_at else None,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@router.get('/')
|
||||||
@router.get("/")
|
async def get_about_content(db: Session=Depends(get_db)):
|
||||||
async def get_about_content(
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Get about page content"""
|
|
||||||
try:
|
try:
|
||||||
content = db.query(PageContent).filter(PageContent.page_type == PageType.ABOUT).first()
|
content = db.query(PageContent).filter(PageContent.page_type == PageType.ABOUT).first()
|
||||||
|
|
||||||
if not content:
|
if not content:
|
||||||
return {
|
return {'status': 'success', 'data': {'page_content': None}}
|
||||||
"status": "success",
|
|
||||||
"data": {
|
|
||||||
"page_content": None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
content_dict = serialize_page_content(content)
|
content_dict = serialize_page_content(content)
|
||||||
|
return {'status': 'success', 'data': {'page_content': content_dict}}
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"data": {
|
|
||||||
"page_content": content_dict
|
|
||||||
}
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error fetching about content: {str(e)}", exc_info=True)
|
logger.error(f'Error fetching about content: {str(e)}', exc_info=True)
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error fetching about content: {str(e)}')
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
detail=f"Error fetching about content: {str(e)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
@@ -1,120 +1,36 @@
|
|||||||
from fastapi import APIRouter, Depends, status
|
from fastapi import APIRouter, Depends, status
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from ..config.database import get_db
|
from ..config.database import get_db
|
||||||
from ..middleware.auth import authorize_roles
|
from ..middleware.auth import authorize_roles
|
||||||
from ..models.user import User
|
from ..models.user import User
|
||||||
from ..schemas.admin_privacy import (
|
from ..schemas.admin_privacy import CookieIntegrationSettings, CookieIntegrationSettingsResponse, CookiePolicySettings, CookiePolicySettingsResponse
|
||||||
CookieIntegrationSettings,
|
|
||||||
CookieIntegrationSettingsResponse,
|
|
||||||
CookiePolicySettings,
|
|
||||||
CookiePolicySettingsResponse,
|
|
||||||
)
|
|
||||||
from ..services.privacy_admin_service import privacy_admin_service
|
from ..services.privacy_admin_service import privacy_admin_service
|
||||||
|
router = APIRouter(prefix='/admin/privacy', tags=['admin-privacy'])
|
||||||
|
|
||||||
|
@router.get('/cookie-policy', response_model=CookiePolicySettingsResponse, status_code=status.HTTP_200_OK)
|
||||||
router = APIRouter(prefix="/admin/privacy", tags=["admin-privacy"])
|
def get_cookie_policy(db: Session=Depends(get_db), _: User=Depends(authorize_roles('admin'))) -> CookiePolicySettingsResponse:
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/cookie-policy",
|
|
||||||
response_model=CookiePolicySettingsResponse,
|
|
||||||
status_code=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
def get_cookie_policy(
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
_: User = Depends(authorize_roles("admin")),
|
|
||||||
) -> CookiePolicySettingsResponse:
|
|
||||||
"""
|
|
||||||
Get global cookie policy configuration (admin only).
|
|
||||||
"""
|
|
||||||
settings = privacy_admin_service.get_policy_settings(db)
|
settings = privacy_admin_service.get_policy_settings(db)
|
||||||
policy = privacy_admin_service.get_or_create_policy(db)
|
policy = privacy_admin_service.get_or_create_policy(db)
|
||||||
updated_by_name = (
|
updated_by_name = policy.updated_by.full_name if getattr(policy, 'updated_by', None) else None
|
||||||
policy.updated_by.full_name if getattr(policy, "updated_by", None) else None
|
return CookiePolicySettingsResponse(data=settings, updated_at=policy.updated_at, updated_by=updated_by_name)
|
||||||
)
|
|
||||||
|
|
||||||
return CookiePolicySettingsResponse(
|
@router.put('/cookie-policy', response_model=CookiePolicySettingsResponse, status_code=status.HTTP_200_OK)
|
||||||
data=settings,
|
def update_cookie_policy(payload: CookiePolicySettings, db: Session=Depends(get_db), current_user: User=Depends(authorize_roles('admin'))) -> CookiePolicySettingsResponse:
|
||||||
updated_at=policy.updated_at,
|
|
||||||
updated_by=updated_by_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.put(
|
|
||||||
"/cookie-policy",
|
|
||||||
response_model=CookiePolicySettingsResponse,
|
|
||||||
status_code=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
def update_cookie_policy(
|
|
||||||
payload: CookiePolicySettings,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user: User = Depends(authorize_roles("admin")),
|
|
||||||
) -> CookiePolicySettingsResponse:
|
|
||||||
"""
|
|
||||||
Update global cookie policy configuration (admin only).
|
|
||||||
"""
|
|
||||||
policy = privacy_admin_service.update_policy(db, payload, current_user)
|
policy = privacy_admin_service.update_policy(db, payload, current_user)
|
||||||
settings = privacy_admin_service.get_policy_settings(db)
|
settings = privacy_admin_service.get_policy_settings(db)
|
||||||
updated_by_name = (
|
updated_by_name = policy.updated_by.full_name if getattr(policy, 'updated_by', None) else None
|
||||||
policy.updated_by.full_name if getattr(policy, "updated_by", None) else None
|
return CookiePolicySettingsResponse(data=settings, updated_at=policy.updated_at, updated_by=updated_by_name)
|
||||||
)
|
|
||||||
|
|
||||||
return CookiePolicySettingsResponse(
|
@router.get('/integrations', response_model=CookieIntegrationSettingsResponse, status_code=status.HTTP_200_OK)
|
||||||
data=settings,
|
def get_cookie_integrations(db: Session=Depends(get_db), _: User=Depends(authorize_roles('admin'))) -> CookieIntegrationSettingsResponse:
|
||||||
updated_at=policy.updated_at,
|
|
||||||
updated_by=updated_by_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/integrations",
|
|
||||||
response_model=CookieIntegrationSettingsResponse,
|
|
||||||
status_code=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
def get_cookie_integrations(
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
_: User = Depends(authorize_roles("admin")),
|
|
||||||
) -> CookieIntegrationSettingsResponse:
|
|
||||||
"""
|
|
||||||
Get IDs for third-party integrations (admin only).
|
|
||||||
"""
|
|
||||||
settings = privacy_admin_service.get_integration_settings(db)
|
settings = privacy_admin_service.get_integration_settings(db)
|
||||||
cfg = privacy_admin_service.get_or_create_integrations(db)
|
cfg = privacy_admin_service.get_or_create_integrations(db)
|
||||||
updated_by_name = (
|
updated_by_name = cfg.updated_by.full_name if getattr(cfg, 'updated_by', None) else None
|
||||||
cfg.updated_by.full_name if getattr(cfg, "updated_by", None) else None
|
return CookieIntegrationSettingsResponse(data=settings, updated_at=cfg.updated_at, updated_by=updated_by_name)
|
||||||
)
|
|
||||||
|
|
||||||
return CookieIntegrationSettingsResponse(
|
@router.put('/integrations', response_model=CookieIntegrationSettingsResponse, status_code=status.HTTP_200_OK)
|
||||||
data=settings,
|
def update_cookie_integrations(payload: CookieIntegrationSettings, db: Session=Depends(get_db), current_user: User=Depends(authorize_roles('admin'))) -> CookieIntegrationSettingsResponse:
|
||||||
updated_at=cfg.updated_at,
|
|
||||||
updated_by=updated_by_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.put(
|
|
||||||
"/integrations",
|
|
||||||
response_model=CookieIntegrationSettingsResponse,
|
|
||||||
status_code=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
def update_cookie_integrations(
|
|
||||||
payload: CookieIntegrationSettings,
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
current_user: User = Depends(authorize_roles("admin")),
|
|
||||||
) -> CookieIntegrationSettingsResponse:
|
|
||||||
"""
|
|
||||||
Update IDs for third-party integrations (admin only).
|
|
||||||
"""
|
|
||||||
cfg = privacy_admin_service.update_integrations(db, payload, current_user)
|
cfg = privacy_admin_service.update_integrations(db, payload, current_user)
|
||||||
settings = privacy_admin_service.get_integration_settings(db)
|
settings = privacy_admin_service.get_integration_settings(db)
|
||||||
updated_by_name = (
|
updated_by_name = cfg.updated_by.full_name if getattr(cfg, 'updated_by', None) else None
|
||||||
cfg.updated_by.full_name if getattr(cfg, "updated_by", None) else None
|
return CookieIntegrationSettingsResponse(data=settings, updated_at=cfg.updated_at, updated_by=updated_by_name)
|
||||||
)
|
|
||||||
|
|
||||||
return CookieIntegrationSettingsResponse(
|
|
||||||
data=settings,
|
|
||||||
updated_at=cfg.updated_at,
|
|
||||||
updated_by=updated_by_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@@ -3,237 +3,91 @@ from sqlalchemy.orm import Session
|
|||||||
from sqlalchemy import desc, or_, func
|
from sqlalchemy import desc, or_, func
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from ..config.database import get_db
|
from ..config.database import get_db
|
||||||
from ..middleware.auth import get_current_user, authorize_roles
|
from ..middleware.auth import get_current_user, authorize_roles
|
||||||
from ..models.user import User
|
from ..models.user import User
|
||||||
from ..models.audit_log import AuditLog
|
from ..models.audit_log import AuditLog
|
||||||
|
router = APIRouter(prefix='/audit-logs', tags=['audit-logs'])
|
||||||
|
|
||||||
router = APIRouter(prefix="/audit-logs", tags=["audit-logs"])
|
@router.get('/')
|
||||||
|
async def get_audit_logs(action: Optional[str]=Query(None, description='Filter by action'), resource_type: Optional[str]=Query(None, description='Filter by resource type'), user_id: Optional[int]=Query(None, description='Filter by user ID'), status_filter: Optional[str]=Query(None, alias='status', description='Filter by status'), search: Optional[str]=Query(None, description='Search in action, resource_type, or details'), start_date: Optional[str]=Query(None, description='Start date (YYYY-MM-DD)'), end_date: Optional[str]=Query(None, description='End date (YYYY-MM-DD)'), page: int=Query(1, ge=1, description='Page number'), limit: int=Query(20, ge=1, le=100, description='Items per page'), current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
||||||
|
|
||||||
@router.get("/")
|
|
||||||
async def get_audit_logs(
|
|
||||||
action: Optional[str] = Query(None, description="Filter by action"),
|
|
||||||
resource_type: Optional[str] = Query(None, description="Filter by resource type"),
|
|
||||||
user_id: Optional[int] = Query(None, description="Filter by user ID"),
|
|
||||||
status_filter: Optional[str] = Query(None, alias="status", description="Filter by status"),
|
|
||||||
search: Optional[str] = Query(None, description="Search in action, resource_type, or details"),
|
|
||||||
start_date: Optional[str] = Query(None, description="Start date (YYYY-MM-DD)"),
|
|
||||||
end_date: Optional[str] = Query(None, description="End date (YYYY-MM-DD)"),
|
|
||||||
page: int = Query(1, ge=1, description="Page number"),
|
|
||||||
limit: int = Query(20, ge=1, le=100, description="Items per page"),
|
|
||||||
current_user: User = Depends(authorize_roles("admin")),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Get audit logs (Admin only)"""
|
|
||||||
try:
|
try:
|
||||||
query = db.query(AuditLog)
|
query = db.query(AuditLog)
|
||||||
|
|
||||||
# Apply filters
|
|
||||||
if action:
|
if action:
|
||||||
query = query.filter(AuditLog.action.like(f"%{action}%"))
|
query = query.filter(AuditLog.action.like(f'%{action}%'))
|
||||||
|
|
||||||
if resource_type:
|
if resource_type:
|
||||||
query = query.filter(AuditLog.resource_type == resource_type)
|
query = query.filter(AuditLog.resource_type == resource_type)
|
||||||
|
|
||||||
if user_id:
|
if user_id:
|
||||||
query = query.filter(AuditLog.user_id == user_id)
|
query = query.filter(AuditLog.user_id == user_id)
|
||||||
|
|
||||||
if status_filter:
|
if status_filter:
|
||||||
query = query.filter(AuditLog.status == status_filter)
|
query = query.filter(AuditLog.status == status_filter)
|
||||||
|
|
||||||
if search:
|
if search:
|
||||||
search_filter = or_(
|
search_filter = or_(AuditLog.action.like(f'%{search}%'), AuditLog.resource_type.like(f'%{search}%'), AuditLog.ip_address.like(f'%{search}%'))
|
||||||
AuditLog.action.like(f"%{search}%"),
|
|
||||||
AuditLog.resource_type.like(f"%{search}%"),
|
|
||||||
AuditLog.ip_address.like(f"%{search}%")
|
|
||||||
)
|
|
||||||
query = query.filter(search_filter)
|
query = query.filter(search_filter)
|
||||||
|
|
||||||
# Date range filter
|
|
||||||
if start_date:
|
if start_date:
|
||||||
try:
|
try:
|
||||||
start = datetime.strptime(start_date, "%Y-%m-%d")
|
start = datetime.strptime(start_date, '%Y-%m-%d')
|
||||||
query = query.filter(AuditLog.created_at >= start)
|
query = query.filter(AuditLog.created_at >= start)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if end_date:
|
if end_date:
|
||||||
try:
|
try:
|
||||||
end = datetime.strptime(end_date, "%Y-%m-%d")
|
end = datetime.strptime(end_date, '%Y-%m-%d')
|
||||||
# Set to end of day
|
|
||||||
end = end.replace(hour=23, minute=59, second=59)
|
end = end.replace(hour=23, minute=59, second=59)
|
||||||
query = query.filter(AuditLog.created_at <= end)
|
query = query.filter(AuditLog.created_at <= end)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Get total count
|
|
||||||
total = query.count()
|
total = query.count()
|
||||||
|
|
||||||
# Apply pagination and ordering
|
|
||||||
offset = (page - 1) * limit
|
offset = (page - 1) * limit
|
||||||
logs = query.order_by(desc(AuditLog.created_at)).offset(offset).limit(limit).all()
|
logs = query.order_by(desc(AuditLog.created_at)).offset(offset).limit(limit).all()
|
||||||
|
|
||||||
# Format response
|
|
||||||
result = []
|
result = []
|
||||||
for log in logs:
|
for log in logs:
|
||||||
log_dict = {
|
log_dict = {'id': log.id, 'user_id': log.user_id, 'action': log.action, 'resource_type': log.resource_type, 'resource_id': log.resource_id, 'ip_address': log.ip_address, 'user_agent': log.user_agent, 'request_id': log.request_id, 'details': log.details, 'status': log.status, 'error_message': log.error_message, 'created_at': log.created_at.isoformat() if log.created_at else None}
|
||||||
"id": log.id,
|
|
||||||
"user_id": log.user_id,
|
|
||||||
"action": log.action,
|
|
||||||
"resource_type": log.resource_type,
|
|
||||||
"resource_id": log.resource_id,
|
|
||||||
"ip_address": log.ip_address,
|
|
||||||
"user_agent": log.user_agent,
|
|
||||||
"request_id": log.request_id,
|
|
||||||
"details": log.details,
|
|
||||||
"status": log.status,
|
|
||||||
"error_message": log.error_message,
|
|
||||||
"created_at": log.created_at.isoformat() if log.created_at else None,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add user info if available
|
|
||||||
if log.user:
|
if log.user:
|
||||||
log_dict["user"] = {
|
log_dict['user'] = {'id': log.user.id, 'full_name': log.user.full_name, 'email': log.user.email}
|
||||||
"id": log.user.id,
|
|
||||||
"full_name": log.user.full_name,
|
|
||||||
"email": log.user.email,
|
|
||||||
}
|
|
||||||
|
|
||||||
result.append(log_dict)
|
result.append(log_dict)
|
||||||
|
return {'status': 'success', 'data': {'logs': result, 'pagination': {'total': total, 'page': page, 'limit': limit, 'totalPages': (total + limit - 1) // limit}}}
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"data": {
|
|
||||||
"logs": result,
|
|
||||||
"pagination": {
|
|
||||||
"total": total,
|
|
||||||
"page": page,
|
|
||||||
"limit": limit,
|
|
||||||
"totalPages": (total + limit - 1) // limit,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.get('/stats')
|
||||||
@router.get("/stats")
|
async def get_audit_stats(start_date: Optional[str]=Query(None, description='Start date (YYYY-MM-DD)'), end_date: Optional[str]=Query(None, description='End date (YYYY-MM-DD)'), current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
||||||
async def get_audit_stats(
|
|
||||||
start_date: Optional[str] = Query(None, description="Start date (YYYY-MM-DD)"),
|
|
||||||
end_date: Optional[str] = Query(None, description="End date (YYYY-MM-DD)"),
|
|
||||||
current_user: User = Depends(authorize_roles("admin")),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Get audit log statistics (Admin only)"""
|
|
||||||
try:
|
try:
|
||||||
query = db.query(AuditLog)
|
query = db.query(AuditLog)
|
||||||
|
|
||||||
# Date range filter
|
|
||||||
if start_date:
|
if start_date:
|
||||||
try:
|
try:
|
||||||
start = datetime.strptime(start_date, "%Y-%m-%d")
|
start = datetime.strptime(start_date, '%Y-%m-%d')
|
||||||
query = query.filter(AuditLog.created_at >= start)
|
query = query.filter(AuditLog.created_at >= start)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if end_date:
|
if end_date:
|
||||||
try:
|
try:
|
||||||
end = datetime.strptime(end_date, "%Y-%m-%d")
|
end = datetime.strptime(end_date, '%Y-%m-%d')
|
||||||
end = end.replace(hour=23, minute=59, second=59)
|
end = end.replace(hour=23, minute=59, second=59)
|
||||||
query = query.filter(AuditLog.created_at <= end)
|
query = query.filter(AuditLog.created_at <= end)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Get statistics
|
|
||||||
total_logs = query.count()
|
total_logs = query.count()
|
||||||
success_count = query.filter(AuditLog.status == "success").count()
|
success_count = query.filter(AuditLog.status == 'success').count()
|
||||||
failed_count = query.filter(AuditLog.status == "failed").count()
|
failed_count = query.filter(AuditLog.status == 'failed').count()
|
||||||
error_count = query.filter(AuditLog.status == "error").count()
|
error_count = query.filter(AuditLog.status == 'error').count()
|
||||||
|
top_actions = db.query(AuditLog.action, func.count(AuditLog.id).label('count')).group_by(AuditLog.action).order_by(desc('count')).limit(10).all()
|
||||||
# Get top actions
|
top_resource_types = db.query(AuditLog.resource_type, func.count(AuditLog.id).label('count')).group_by(AuditLog.resource_type).order_by(desc('count')).limit(10).all()
|
||||||
top_actions = (
|
return {'status': 'success', 'data': {'total': total_logs, 'by_status': {'success': success_count, 'failed': failed_count, 'error': error_count}, 'top_actions': [{'action': action, 'count': count} for action, count in top_actions], 'top_resource_types': [{'resource_type': rt, 'count': count} for rt, count in top_resource_types]}}
|
||||||
db.query(
|
|
||||||
AuditLog.action,
|
|
||||||
func.count(AuditLog.id).label("count")
|
|
||||||
)
|
|
||||||
.group_by(AuditLog.action)
|
|
||||||
.order_by(desc("count"))
|
|
||||||
.limit(10)
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get top resource types
|
|
||||||
top_resource_types = (
|
|
||||||
db.query(
|
|
||||||
AuditLog.resource_type,
|
|
||||||
func.count(AuditLog.id).label("count")
|
|
||||||
)
|
|
||||||
.group_by(AuditLog.resource_type)
|
|
||||||
.order_by(desc("count"))
|
|
||||||
.limit(10)
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"data": {
|
|
||||||
"total": total_logs,
|
|
||||||
"by_status": {
|
|
||||||
"success": success_count,
|
|
||||||
"failed": failed_count,
|
|
||||||
"error": error_count,
|
|
||||||
},
|
|
||||||
"top_actions": [{"action": action, "count": count} for action, count in top_actions],
|
|
||||||
"top_resource_types": [{"resource_type": rt, "count": count} for rt, count in top_resource_types],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.get('/{id}')
|
||||||
@router.get("/{id}")
|
async def get_audit_log_by_id(id: int, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
||||||
async def get_audit_log_by_id(
|
|
||||||
id: int,
|
|
||||||
current_user: User = Depends(authorize_roles("admin")),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Get audit log by ID (Admin only)"""
|
|
||||||
try:
|
try:
|
||||||
log = db.query(AuditLog).filter(AuditLog.id == id).first()
|
log = db.query(AuditLog).filter(AuditLog.id == id).first()
|
||||||
|
|
||||||
if not log:
|
if not log:
|
||||||
raise HTTPException(status_code=404, detail="Audit log not found")
|
raise HTTPException(status_code=404, detail='Audit log not found')
|
||||||
|
log_dict = {'id': log.id, 'user_id': log.user_id, 'action': log.action, 'resource_type': log.resource_type, 'resource_id': log.resource_id, 'ip_address': log.ip_address, 'user_agent': log.user_agent, 'request_id': log.request_id, 'details': log.details, 'status': log.status, 'error_message': log.error_message, 'created_at': log.created_at.isoformat() if log.created_at else None}
|
||||||
log_dict = {
|
|
||||||
"id": log.id,
|
|
||||||
"user_id": log.user_id,
|
|
||||||
"action": log.action,
|
|
||||||
"resource_type": log.resource_type,
|
|
||||||
"resource_id": log.resource_id,
|
|
||||||
"ip_address": log.ip_address,
|
|
||||||
"user_agent": log.user_agent,
|
|
||||||
"request_id": log.request_id,
|
|
||||||
"details": log.details,
|
|
||||||
"status": log.status,
|
|
||||||
"error_message": log.error_message,
|
|
||||||
"created_at": log.created_at.isoformat() if log.created_at else None,
|
|
||||||
}
|
|
||||||
|
|
||||||
if log.user:
|
if log.user:
|
||||||
log_dict["user"] = {
|
log_dict['user'] = {'id': log.user.id, 'full_name': log.user.full_name, 'email': log.user.email}
|
||||||
"id": log.user.id,
|
return {'status': 'success', 'data': {'log': log_dict}}
|
||||||
"full_name": log.user.full_name,
|
|
||||||
"email": log.user.email,
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"data": {"log": log_dict}
|
|
||||||
}
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
@@ -5,518 +5,193 @@ from pathlib import Path
|
|||||||
import aiofiles
|
import aiofiles
|
||||||
import uuid
|
import uuid
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from ..config.database import get_db
|
from ..config.database import get_db
|
||||||
from ..services.auth_service import auth_service
|
from ..services.auth_service import auth_service
|
||||||
from ..schemas.auth import (
|
from ..schemas.auth import RegisterRequest, LoginRequest, RefreshTokenRequest, ForgotPasswordRequest, ResetPasswordRequest, AuthResponse, TokenResponse, MessageResponse, MFAInitResponse, EnableMFARequest, VerifyMFARequest, MFAStatusResponse
|
||||||
RegisterRequest,
|
|
||||||
LoginRequest,
|
|
||||||
RefreshTokenRequest,
|
|
||||||
ForgotPasswordRequest,
|
|
||||||
ResetPasswordRequest,
|
|
||||||
AuthResponse,
|
|
||||||
TokenResponse,
|
|
||||||
MessageResponse,
|
|
||||||
MFAInitResponse,
|
|
||||||
EnableMFARequest,
|
|
||||||
VerifyMFARequest,
|
|
||||||
MFAStatusResponse
|
|
||||||
)
|
|
||||||
from ..middleware.auth import get_current_user
|
from ..middleware.auth import get_current_user
|
||||||
from ..models.user import User
|
from ..models.user import User
|
||||||
|
router = APIRouter(prefix='/auth', tags=['auth'])
|
||||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
|
||||||
|
|
||||||
|
|
||||||
def get_base_url(request: Request) -> str:
|
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:8000')}'
|
||||||
return os.getenv("SERVER_URL") or f"http://{request.headers.get('host', 'localhost:8000')}"
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_image_url(image_url: str, base_url: str) -> str:
|
def normalize_image_url(image_url: str, base_url: str) -> str:
|
||||||
"""Normalize image URL to absolute URL"""
|
|
||||||
if not image_url:
|
if not image_url:
|
||||||
return image_url
|
return image_url
|
||||||
if image_url.startswith('http://') or image_url.startswith('https://'):
|
if image_url.startswith('http://') or image_url.startswith('https://'):
|
||||||
return image_url
|
return image_url
|
||||||
if image_url.startswith('/'):
|
if image_url.startswith('/'):
|
||||||
return f"{base_url}{image_url}"
|
return f'{base_url}{image_url}'
|
||||||
return f"{base_url}/{image_url}"
|
return f'{base_url}/{image_url}'
|
||||||
|
|
||||||
|
@router.post('/register', status_code=status.HTTP_201_CREATED)
|
||||||
@router.post("/register", status_code=status.HTTP_201_CREATED)
|
async def register(request: RegisterRequest, response: Response, db: Session=Depends(get_db)):
|
||||||
async def register(
|
|
||||||
request: RegisterRequest,
|
|
||||||
response: Response,
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Register new user"""
|
|
||||||
try:
|
try:
|
||||||
result = await auth_service.register(
|
result = await auth_service.register(db=db, name=request.name, email=request.email, password=request.password, phone=request.phone)
|
||||||
db=db,
|
response.set_cookie(key='refreshToken', value=result['refreshToken'], httponly=True, secure=False, samesite='strict', max_age=7 * 24 * 60 * 60, path='/')
|
||||||
name=request.name,
|
return {'status': 'success', 'message': 'Registration successful', 'data': {'token': result['token'], 'user': result['user']}}
|
||||||
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:
|
except ValueError as e:
|
||||||
error_message = str(e)
|
error_message = str(e)
|
||||||
return JSONResponse(
|
return JSONResponse(status_code=status.HTTP_400_BAD_REQUEST, content={'status': 'error', 'message': error_message})
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
content={
|
|
||||||
"status": "error",
|
|
||||||
"message": error_message
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
@router.post('/login')
|
||||||
@router.post("/login")
|
async def login(request: LoginRequest, response: Response, db: Session=Depends(get_db)):
|
||||||
async def login(
|
|
||||||
request: LoginRequest,
|
|
||||||
response: Response,
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Login user"""
|
|
||||||
try:
|
try:
|
||||||
result = await auth_service.login(
|
result = await auth_service.login(db=db, email=request.email, password=request.password, remember_me=request.rememberMe or False, mfa_token=request.mfaToken)
|
||||||
db=db,
|
if result.get('requires_mfa'):
|
||||||
email=request.email,
|
return {'status': 'success', 'requires_mfa': True, 'user_id': result['user_id']}
|
||||||
password=request.password,
|
|
||||||
remember_me=request.rememberMe or False,
|
|
||||||
mfa_token=request.mfaToken
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check if MFA is required
|
|
||||||
if result.get("requires_mfa"):
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"requires_mfa": True,
|
|
||||||
"user_id": result["user_id"]
|
|
||||||
}
|
|
||||||
|
|
||||||
# Set refresh token as HttpOnly cookie
|
|
||||||
max_age = 7 * 24 * 60 * 60 if request.rememberMe else 1 * 24 * 60 * 60
|
max_age = 7 * 24 * 60 * 60 if request.rememberMe else 1 * 24 * 60 * 60
|
||||||
response.set_cookie(
|
response.set_cookie(key='refreshToken', value=result['refreshToken'], httponly=True, secure=False, samesite='strict', max_age=max_age, path='/')
|
||||||
key="refreshToken",
|
return {'status': 'success', 'data': {'token': result['token'], 'user': result['user']}}
|
||||||
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:
|
except ValueError as e:
|
||||||
error_message = str(e)
|
error_message = str(e)
|
||||||
status_code = status.HTTP_401_UNAUTHORIZED if "Invalid email or password" in error_message or "Invalid MFA token" in error_message else status.HTTP_400_BAD_REQUEST
|
status_code = status.HTTP_401_UNAUTHORIZED if 'Invalid email or password' in error_message or 'Invalid MFA token' in error_message else status.HTTP_400_BAD_REQUEST
|
||||||
return JSONResponse(
|
return JSONResponse(status_code=status_code, content={'status': 'error', 'message': error_message})
|
||||||
status_code=status_code,
|
|
||||||
content={
|
|
||||||
"status": "error",
|
|
||||||
"message": error_message
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
@router.post('/refresh-token', response_model=TokenResponse)
|
||||||
@router.post("/refresh-token", response_model=TokenResponse)
|
async def refresh_token(refreshToken: str=Cookie(None), db: Session=Depends(get_db)):
|
||||||
async def refresh_token(
|
|
||||||
refreshToken: str = Cookie(None),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Refresh access token"""
|
|
||||||
if not refreshToken:
|
if not refreshToken:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Refresh token not found')
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
||||||
detail="Refresh token not found"
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = await auth_service.refresh_access_token(db, refreshToken)
|
result = await auth_service.refresh_access_token(db, refreshToken)
|
||||||
return result
|
return result
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(e))
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
||||||
detail=str(e)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
@router.post('/logout', response_model=MessageResponse)
|
||||||
@router.post("/logout", response_model=MessageResponse)
|
async def logout(response: Response, refreshToken: str=Cookie(None), db: Session=Depends(get_db)):
|
||||||
async def logout(
|
|
||||||
response: Response,
|
|
||||||
refreshToken: str = Cookie(None),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Logout user"""
|
|
||||||
if refreshToken:
|
if refreshToken:
|
||||||
await auth_service.logout(db, refreshToken)
|
await auth_service.logout(db, refreshToken)
|
||||||
|
response.delete_cookie(key='refreshToken', path='/')
|
||||||
|
return {'status': 'success', 'message': 'Logout successful'}
|
||||||
|
|
||||||
# Clear refresh token cookie
|
@router.get('/profile')
|
||||||
response.delete_cookie(key="refreshToken", path="/")
|
async def get_profile(current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||||
|
|
||||||
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:
|
try:
|
||||||
user = await auth_service.get_profile(db, current_user.id)
|
user = await auth_service.get_profile(db, current_user.id)
|
||||||
return {
|
return {'status': 'success', 'data': {'user': user}}
|
||||||
"status": "success",
|
|
||||||
"data": {
|
|
||||||
"user": user
|
|
||||||
}
|
|
||||||
}
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
if "User not found" in str(e):
|
if 'User not found' in str(e):
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||||
detail=str(e)
|
|
||||||
)
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail=str(e)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
@router.put('/profile')
|
||||||
@router.put("/profile")
|
async def update_profile(profile_data: dict, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||||
async def update_profile(
|
|
||||||
profile_data: dict,
|
|
||||||
current_user: User = Depends(get_current_user),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Update current user profile"""
|
|
||||||
try:
|
try:
|
||||||
user = await auth_service.update_profile(
|
user = await auth_service.update_profile(db=db, user_id=current_user.id, full_name=profile_data.get('full_name'), email=profile_data.get('email'), phone_number=profile_data.get('phone_number'), password=profile_data.get('password'), current_password=profile_data.get('currentPassword'), currency=profile_data.get('currency'))
|
||||||
db=db,
|
return {'status': 'success', 'message': 'Profile updated successfully', 'data': {'user': user}}
|
||||||
user_id=current_user.id,
|
|
||||||
full_name=profile_data.get("full_name"),
|
|
||||||
email=profile_data.get("email"),
|
|
||||||
phone_number=profile_data.get("phone_number"),
|
|
||||||
password=profile_data.get("password"),
|
|
||||||
current_password=profile_data.get("currentPassword"),
|
|
||||||
currency=profile_data.get("currency")
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"message": "Profile updated successfully",
|
|
||||||
"data": {
|
|
||||||
"user": user
|
|
||||||
}
|
|
||||||
}
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
error_message = str(e)
|
error_message = str(e)
|
||||||
status_code = status.HTTP_400_BAD_REQUEST
|
status_code = status.HTTP_400_BAD_REQUEST
|
||||||
if "not found" in error_message.lower():
|
if 'not found' in error_message.lower():
|
||||||
status_code = status.HTTP_404_NOT_FOUND
|
status_code = status.HTTP_404_NOT_FOUND
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status_code, detail=error_message)
|
||||||
status_code=status_code,
|
|
||||||
detail=error_message
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'An error occurred: {str(e)}')
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
detail=f"An error occurred: {str(e)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
@router.post('/forgot-password', response_model=MessageResponse)
|
||||||
@router.post("/forgot-password", response_model=MessageResponse)
|
async def forgot_password(request: ForgotPasswordRequest, db: Session=Depends(get_db)):
|
||||||
async def forgot_password(
|
|
||||||
request: ForgotPasswordRequest,
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Send password reset link"""
|
|
||||||
result = await auth_service.forgot_password(db, request.email)
|
result = await auth_service.forgot_password(db, request.email)
|
||||||
return {
|
return {'status': 'success', 'message': result['message']}
|
||||||
"status": "success",
|
|
||||||
"message": result["message"]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@router.post('/reset-password', response_model=MessageResponse)
|
||||||
@router.post("/reset-password", response_model=MessageResponse)
|
async def reset_password(request: ResetPasswordRequest, db: Session=Depends(get_db)):
|
||||||
async def reset_password(
|
|
||||||
request: ResetPasswordRequest,
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Reset password with token"""
|
|
||||||
try:
|
try:
|
||||||
result = await auth_service.reset_password(
|
result = await auth_service.reset_password(db=db, token=request.token, password=request.password)
|
||||||
db=db,
|
return {'status': 'success', 'message': result['message']}
|
||||||
token=request.token,
|
|
||||||
password=request.password
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"message": result["message"]
|
|
||||||
}
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
status_code = status.HTTP_400_BAD_REQUEST
|
status_code = status.HTTP_400_BAD_REQUEST
|
||||||
if "User not found" in str(e):
|
if 'User not found' in str(e):
|
||||||
status_code = status.HTTP_404_NOT_FOUND
|
status_code = status.HTTP_404_NOT_FOUND
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status_code, detail=str(e))
|
||||||
status_code=status_code,
|
|
||||||
detail=str(e)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# MFA Routes
|
|
||||||
from ..services.mfa_service import mfa_service
|
from ..services.mfa_service import mfa_service
|
||||||
from ..config.settings import settings
|
from ..config.settings import settings
|
||||||
|
|
||||||
|
@router.get('/mfa/init')
|
||||||
@router.get("/mfa/init")
|
async def init_mfa(current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||||
async def init_mfa(
|
|
||||||
current_user: User = Depends(get_current_user),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Initialize MFA setup - generate secret and QR code"""
|
|
||||||
try:
|
try:
|
||||||
if current_user.mfa_enabled:
|
if current_user.mfa_enabled:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='MFA is already enabled')
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail="MFA is already enabled"
|
|
||||||
)
|
|
||||||
|
|
||||||
secret = mfa_service.generate_secret()
|
secret = mfa_service.generate_secret()
|
||||||
app_name = getattr(settings, 'APP_NAME', 'Hotel Booking')
|
app_name = getattr(settings, 'APP_NAME', 'Hotel Booking')
|
||||||
qr_code = mfa_service.generate_qr_code(secret, current_user.email, app_name)
|
qr_code = mfa_service.generate_qr_code(secret, current_user.email, app_name)
|
||||||
|
return {'status': 'success', 'data': {'secret': secret, 'qr_code': qr_code}}
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"data": {
|
|
||||||
"secret": secret,
|
|
||||||
"qr_code": qr_code
|
|
||||||
}
|
|
||||||
}
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error initializing MFA: {str(e)}')
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
detail=f"Error initializing MFA: {str(e)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
@router.post('/mfa/enable')
|
||||||
@router.post("/mfa/enable")
|
async def enable_mfa(request: EnableMFARequest, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||||
async def enable_mfa(
|
|
||||||
request: EnableMFARequest,
|
|
||||||
current_user: User = Depends(get_current_user),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Enable MFA after verifying token"""
|
|
||||||
try:
|
try:
|
||||||
success, backup_codes = mfa_service.enable_mfa(
|
success, backup_codes = mfa_service.enable_mfa(db=db, user_id=current_user.id, secret=request.secret, verification_token=request.verification_token)
|
||||||
db=db,
|
return {'status': 'success', 'message': 'MFA enabled successfully', 'data': {'backup_codes': backup_codes}}
|
||||||
user_id=current_user.id,
|
|
||||||
secret=request.secret,
|
|
||||||
verification_token=request.verification_token
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"message": "MFA enabled successfully",
|
|
||||||
"data": {
|
|
||||||
"backup_codes": backup_codes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail=str(e)
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error enabling MFA: {str(e)}')
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
detail=f"Error enabling MFA: {str(e)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
@router.post('/mfa/disable')
|
||||||
@router.post("/mfa/disable")
|
async def disable_mfa(current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||||
async def disable_mfa(
|
|
||||||
current_user: User = Depends(get_current_user),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Disable MFA"""
|
|
||||||
try:
|
try:
|
||||||
mfa_service.disable_mfa(db=db, user_id=current_user.id)
|
mfa_service.disable_mfa(db=db, user_id=current_user.id)
|
||||||
return {
|
return {'status': 'success', 'message': 'MFA disabled successfully'}
|
||||||
"status": "success",
|
|
||||||
"message": "MFA disabled successfully"
|
|
||||||
}
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail=str(e)
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error disabling MFA: {str(e)}')
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
detail=f"Error disabling MFA: {str(e)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
@router.get('/mfa/status', response_model=MFAStatusResponse)
|
||||||
@router.get("/mfa/status", response_model=MFAStatusResponse)
|
async def get_mfa_status(current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||||
async def get_mfa_status(
|
|
||||||
current_user: User = Depends(get_current_user),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Get MFA status for current user"""
|
|
||||||
try:
|
try:
|
||||||
status_data = mfa_service.get_mfa_status(db=db, user_id=current_user.id)
|
status_data = mfa_service.get_mfa_status(db=db, user_id=current_user.id)
|
||||||
return status_data
|
return status_data
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail=str(e)
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error getting MFA status: {str(e)}')
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
detail=f"Error getting MFA status: {str(e)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
@router.post('/mfa/regenerate-backup-codes')
|
||||||
@router.post("/mfa/regenerate-backup-codes")
|
async def regenerate_backup_codes(current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||||
async def regenerate_backup_codes(
|
|
||||||
current_user: User = Depends(get_current_user),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Regenerate backup codes for MFA"""
|
|
||||||
try:
|
try:
|
||||||
backup_codes = mfa_service.regenerate_backup_codes(db=db, user_id=current_user.id)
|
backup_codes = mfa_service.regenerate_backup_codes(db=db, user_id=current_user.id)
|
||||||
return {
|
return {'status': 'success', 'message': 'Backup codes regenerated successfully', 'data': {'backup_codes': backup_codes}}
|
||||||
"status": "success",
|
|
||||||
"message": "Backup codes regenerated successfully",
|
|
||||||
"data": {
|
|
||||||
"backup_codes": backup_codes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail=str(e)
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error regenerating backup codes: {str(e)}')
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
detail=f"Error regenerating backup codes: {str(e)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
@router.post('/avatar/upload')
|
||||||
@router.post("/avatar/upload")
|
async def upload_avatar(request: Request, image: UploadFile=File(...), current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||||
async def upload_avatar(
|
|
||||||
request: Request,
|
|
||||||
image: UploadFile = File(...),
|
|
||||||
current_user: User = Depends(get_current_user),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Upload user avatar"""
|
|
||||||
try:
|
try:
|
||||||
# Validate file type
|
|
||||||
if not image.content_type or not image.content_type.startswith('image/'):
|
if not image.content_type or not image.content_type.startswith('image/'):
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='File must be an image')
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail="File must be an image"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validate file size (max 2MB)
|
|
||||||
content = await image.read()
|
content = await image.read()
|
||||||
if len(content) > 2 * 1024 * 1024: # 2MB
|
if len(content) > 2 * 1024 * 1024:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Avatar file size must be less than 2MB')
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
upload_dir = Path(__file__).parent.parent.parent / 'uploads' / 'avatars'
|
||||||
detail="Avatar file size must be less than 2MB"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create uploads directory
|
|
||||||
upload_dir = Path(__file__).parent.parent.parent / "uploads" / "avatars"
|
|
||||||
upload_dir.mkdir(parents=True, exist_ok=True)
|
upload_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Delete old avatar if exists
|
|
||||||
if current_user.avatar:
|
if current_user.avatar:
|
||||||
old_avatar_path = Path(__file__).parent.parent.parent / current_user.avatar.lstrip('/')
|
old_avatar_path = Path(__file__).parent.parent.parent / current_user.avatar.lstrip('/')
|
||||||
if old_avatar_path.exists() and old_avatar_path.is_file():
|
if old_avatar_path.exists() and old_avatar_path.is_file():
|
||||||
try:
|
try:
|
||||||
old_avatar_path.unlink()
|
old_avatar_path.unlink()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass # Ignore deletion errors
|
pass
|
||||||
|
|
||||||
# Generate filename
|
|
||||||
ext = Path(image.filename).suffix or '.png'
|
ext = Path(image.filename).suffix or '.png'
|
||||||
filename = f"avatar-{current_user.id}-{uuid.uuid4()}{ext}"
|
filename = f'avatar-{current_user.id}-{uuid.uuid4()}{ext}'
|
||||||
file_path = upload_dir / filename
|
file_path = upload_dir / filename
|
||||||
|
|
||||||
# Save file
|
|
||||||
async with aiofiles.open(file_path, 'wb') as f:
|
async with aiofiles.open(file_path, 'wb') as f:
|
||||||
await f.write(content)
|
await f.write(content)
|
||||||
|
image_url = f'/uploads/avatars/{filename}'
|
||||||
# Update user avatar
|
|
||||||
image_url = f"/uploads/avatars/{filename}"
|
|
||||||
current_user.avatar = image_url
|
current_user.avatar = image_url
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(current_user)
|
db.refresh(current_user)
|
||||||
|
|
||||||
# Return the image URL
|
|
||||||
base_url = get_base_url(request)
|
base_url = get_base_url(request)
|
||||||
full_url = normalize_image_url(image_url, base_url)
|
full_url = normalize_image_url(image_url, base_url)
|
||||||
|
return {'success': True, 'status': 'success', 'message': 'Avatar uploaded successfully', 'data': {'avatar_url': image_url, 'full_url': full_url, 'user': {'id': current_user.id, 'name': current_user.full_name, 'email': current_user.email, 'phone': current_user.phone, 'avatar': image_url, 'role': current_user.role.name if current_user.role else 'customer'}}}
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"status": "success",
|
|
||||||
"message": "Avatar uploaded successfully",
|
|
||||||
"data": {
|
|
||||||
"avatar_url": image_url,
|
|
||||||
"full_url": full_url,
|
|
||||||
"user": {
|
|
||||||
"id": current_user.id,
|
|
||||||
"name": current_user.full_name,
|
|
||||||
"email": current_user.email,
|
|
||||||
"phone": current_user.phone,
|
|
||||||
"avatar": image_url,
|
|
||||||
"role": current_user.role.name if current_user.role else "customer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error uploading avatar: {str(e)}')
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
detail=f"Error uploading avatar: {str(e)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
@@ -7,300 +7,144 @@ from pathlib import Path
|
|||||||
import os
|
import os
|
||||||
import aiofiles
|
import aiofiles
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from ..config.database import get_db
|
from ..config.database import get_db
|
||||||
from ..middleware.auth import get_current_user, authorize_roles
|
from ..middleware.auth import get_current_user, authorize_roles
|
||||||
from ..models.user import User
|
from ..models.user import User
|
||||||
from ..models.banner import Banner
|
from ..models.banner import Banner
|
||||||
|
router = APIRouter(prefix='/banners', tags=['banners'])
|
||||||
router = APIRouter(prefix="/banners", tags=["banners"])
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_image_url(image_url: str, base_url: str) -> str:
|
def normalize_image_url(image_url: str, base_url: str) -> str:
|
||||||
"""Normalize image URL to absolute URL"""
|
|
||||||
if not image_url:
|
if not image_url:
|
||||||
return image_url
|
return image_url
|
||||||
if image_url.startswith('http://') or image_url.startswith('https://'):
|
if image_url.startswith('http://') or image_url.startswith('https://'):
|
||||||
return image_url
|
return image_url
|
||||||
if image_url.startswith('/'):
|
if image_url.startswith('/'):
|
||||||
return f"{base_url}{image_url}"
|
return f'{base_url}{image_url}'
|
||||||
return f"{base_url}/{image_url}"
|
return f'{base_url}/{image_url}'
|
||||||
|
|
||||||
|
|
||||||
def get_base_url(request: Request) -> str:
|
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')}'
|
||||||
return os.getenv("SERVER_URL") or f"http://{request.headers.get('host', 'localhost:3000')}"
|
|
||||||
|
|
||||||
|
@router.get('/')
|
||||||
@router.get("/")
|
async def get_banners(request: Request, position: Optional[str]=Query(None), db: Session=Depends(get_db)):
|
||||||
async def get_banners(
|
|
||||||
request: Request,
|
|
||||||
position: Optional[str] = Query(None),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Get all active banners"""
|
|
||||||
try:
|
try:
|
||||||
query = db.query(Banner).filter(Banner.is_active == True)
|
query = db.query(Banner).filter(Banner.is_active == True)
|
||||||
|
|
||||||
# Filter by position
|
|
||||||
if position:
|
if position:
|
||||||
query = query.filter(Banner.position == position)
|
query = query.filter(Banner.position == position)
|
||||||
|
|
||||||
# Filter by date range
|
|
||||||
now = datetime.utcnow()
|
now = datetime.utcnow()
|
||||||
query = query.filter(
|
query = query.filter(or_(Banner.start_date == None, Banner.start_date <= now)).filter(or_(Banner.end_date == None, Banner.end_date >= now))
|
||||||
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()
|
banners = query.order_by(Banner.display_order.asc(), Banner.created_at.desc()).all()
|
||||||
|
|
||||||
base_url = get_base_url(request)
|
base_url = get_base_url(request)
|
||||||
result = []
|
result = []
|
||||||
for banner in banners:
|
for banner in banners:
|
||||||
banner_dict = {
|
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}
|
||||||
"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)
|
result.append(banner_dict)
|
||||||
|
return {'status': 'success', 'data': {'banners': result}}
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"data": {"banners": result}
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.get('/{id}')
|
||||||
@router.get("/{id}")
|
async def get_banner_by_id(id: int, request: Request, db: Session=Depends(get_db)):
|
||||||
async def get_banner_by_id(
|
|
||||||
id: int,
|
|
||||||
request: Request,
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Get banner by ID"""
|
|
||||||
try:
|
try:
|
||||||
banner = db.query(Banner).filter(Banner.id == id).first()
|
banner = db.query(Banner).filter(Banner.id == id).first()
|
||||||
if not banner:
|
if not banner:
|
||||||
raise HTTPException(status_code=404, detail="Banner not found")
|
raise HTTPException(status_code=404, detail='Banner not found')
|
||||||
|
|
||||||
base_url = get_base_url(request)
|
base_url = get_base_url(request)
|
||||||
banner_dict = {
|
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}
|
||||||
"id": banner.id,
|
return {'status': 'success', 'data': {'banner': banner_dict}}
|
||||||
"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:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.post('/', dependencies=[Depends(authorize_roles('admin'))])
|
||||||
@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)):
|
||||||
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:
|
try:
|
||||||
banner = Banner(
|
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)
|
||||||
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.add(banner)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(banner)
|
db.refresh(banner)
|
||||||
|
return {'status': 'success', 'message': 'Banner created successfully', 'data': {'banner': banner}}
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"message": "Banner created successfully",
|
|
||||||
"data": {"banner": banner}
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.put('/{id}', dependencies=[Depends(authorize_roles('admin'))])
|
||||||
@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)):
|
||||||
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:
|
try:
|
||||||
banner = db.query(Banner).filter(Banner.id == id).first()
|
banner = db.query(Banner).filter(Banner.id == id).first()
|
||||||
if not banner:
|
if not banner:
|
||||||
raise HTTPException(status_code=404, detail="Banner not found")
|
raise HTTPException(status_code=404, detail='Banner not found')
|
||||||
|
if 'title' in banner_data:
|
||||||
if "title" in banner_data:
|
banner.title = banner_data['title']
|
||||||
banner.title = banner_data["title"]
|
if 'description' in banner_data:
|
||||||
if "description" in banner_data:
|
banner.description = banner_data['description']
|
||||||
banner.description = banner_data["description"]
|
if 'image_url' in banner_data:
|
||||||
if "image_url" in banner_data:
|
banner.image_url = banner_data['image_url']
|
||||||
banner.image_url = banner_data["image_url"]
|
if 'link' in banner_data:
|
||||||
if "link" in banner_data:
|
banner.link_url = banner_data['link']
|
||||||
banner.link_url = banner_data["link"]
|
if 'position' in banner_data:
|
||||||
if "position" in banner_data:
|
banner.position = banner_data['position']
|
||||||
banner.position = banner_data["position"]
|
if 'display_order' in banner_data:
|
||||||
if "display_order" in banner_data:
|
banner.display_order = banner_data['display_order']
|
||||||
banner.display_order = banner_data["display_order"]
|
if 'is_active' in banner_data:
|
||||||
if "is_active" in banner_data:
|
banner.is_active = banner_data['is_active']
|
||||||
banner.is_active = banner_data["is_active"]
|
if 'start_date' in banner_data:
|
||||||
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
|
||||||
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:
|
||||||
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
|
||||||
banner.end_date = datetime.fromisoformat(banner_data["end_date"].replace('Z', '+00:00')) if banner_data["end_date"] else None
|
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(banner)
|
db.refresh(banner)
|
||||||
|
return {'status': 'success', 'message': 'Banner updated successfully', 'data': {'banner': banner}}
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"message": "Banner updated successfully",
|
|
||||||
"data": {"banner": banner}
|
|
||||||
}
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.delete('/{id}', dependencies=[Depends(authorize_roles('admin'))])
|
||||||
@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)):
|
||||||
async def delete_banner(
|
|
||||||
id: int,
|
|
||||||
current_user: User = Depends(authorize_roles("admin")),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Delete banner (Admin only)"""
|
|
||||||
try:
|
try:
|
||||||
banner = db.query(Banner).filter(Banner.id == id).first()
|
banner = db.query(Banner).filter(Banner.id == id).first()
|
||||||
if not banner:
|
if not banner:
|
||||||
raise HTTPException(status_code=404, detail="Banner not found")
|
raise HTTPException(status_code=404, detail='Banner not found')
|
||||||
|
|
||||||
# Delete image file if it exists and is a local upload
|
|
||||||
if banner.image_url and banner.image_url.startswith('/uploads/banners/'):
|
if banner.image_url and banner.image_url.startswith('/uploads/banners/'):
|
||||||
file_path = Path(__file__).parent.parent.parent / "uploads" / "banners" / Path(banner.image_url).name
|
file_path = Path(__file__).parent.parent.parent / 'uploads' / 'banners' / Path(banner.image_url).name
|
||||||
if file_path.exists():
|
if file_path.exists():
|
||||||
file_path.unlink()
|
file_path.unlink()
|
||||||
|
|
||||||
db.delete(banner)
|
db.delete(banner)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
return {'status': 'success', 'message': 'Banner deleted successfully'}
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"message": "Banner deleted successfully"
|
|
||||||
}
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.post('/upload', dependencies=[Depends(authorize_roles('admin'))])
|
||||||
@router.post("/upload", dependencies=[Depends(authorize_roles("admin"))])
|
async def upload_banner_image(request: Request, image: UploadFile=File(...), current_user: User=Depends(authorize_roles('admin'))):
|
||||||
async def upload_banner_image(
|
|
||||||
request: Request,
|
|
||||||
image: UploadFile = File(...),
|
|
||||||
current_user: User = Depends(authorize_roles("admin")),
|
|
||||||
):
|
|
||||||
"""Upload banner image (Admin only)"""
|
|
||||||
try:
|
try:
|
||||||
# Validate file exists
|
|
||||||
if not image:
|
if not image:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='No file provided')
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail="No file provided"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validate file type
|
|
||||||
if not image.content_type or not image.content_type.startswith('image/'):
|
if not image.content_type or not image.content_type.startswith('image/'):
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f'File must be an image. Received: {image.content_type}')
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail=f"File must be an image. Received: {image.content_type}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Validate filename
|
|
||||||
if not image.filename:
|
if not image.filename:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Filename is required')
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
upload_dir = Path(__file__).parent.parent.parent / 'uploads' / 'banners'
|
||||||
detail="Filename is required"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create uploads directory
|
|
||||||
upload_dir = Path(__file__).parent.parent.parent / "uploads" / "banners"
|
|
||||||
upload_dir.mkdir(parents=True, exist_ok=True)
|
upload_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Generate filename
|
|
||||||
ext = Path(image.filename).suffix or '.jpg'
|
ext = Path(image.filename).suffix or '.jpg'
|
||||||
filename = f"banner-{uuid.uuid4()}{ext}"
|
filename = f'banner-{uuid.uuid4()}{ext}'
|
||||||
file_path = upload_dir / filename
|
file_path = upload_dir / filename
|
||||||
|
|
||||||
# Save file
|
|
||||||
async with aiofiles.open(file_path, 'wb') as f:
|
async with aiofiles.open(file_path, 'wb') as f:
|
||||||
content = await image.read()
|
content = await image.read()
|
||||||
if not content:
|
if not content:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='File is empty')
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
|
||||||
detail="File is empty"
|
|
||||||
)
|
|
||||||
await f.write(content)
|
await f.write(content)
|
||||||
|
image_url = f'/uploads/banners/{filename}'
|
||||||
# Return the image URL
|
|
||||||
image_url = f"/uploads/banners/{filename}"
|
|
||||||
base_url = get_base_url(request)
|
base_url = get_base_url(request)
|
||||||
full_url = normalize_image_url(image_url, base_url)
|
full_url = normalize_image_url(image_url, base_url)
|
||||||
|
return {'success': True, 'status': 'success', 'message': 'Image uploaded successfully', 'data': {'image_url': image_url, 'full_url': full_url}}
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"status": "success",
|
|
||||||
"message": "Image uploaded successfully",
|
|
||||||
"data": {
|
|
||||||
"image_url": image_url,
|
|
||||||
"full_url": full_url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
File diff suppressed because it is too large
Load Diff
335
Backend/src/routes/chat_routes.py
Normal file
335
Backend/src/routes/chat_routes.py
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException, status, WebSocket, WebSocketDisconnect
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import and_, or_
|
||||||
|
from typing import List, Optional
|
||||||
|
from datetime import datetime
|
||||||
|
import json
|
||||||
|
from ..config.database import get_db
|
||||||
|
from ..middleware.auth import get_current_user, get_current_user_optional
|
||||||
|
from ..models.user import User
|
||||||
|
from ..models.chat import Chat, ChatMessage, ChatStatus
|
||||||
|
from ..models.role import Role
|
||||||
|
router = APIRouter(prefix='/chat', tags=['chat'])
|
||||||
|
|
||||||
|
class ConnectionManager:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.active_connections: dict[int, List[WebSocket]] = {}
|
||||||
|
self.staff_connections: dict[int, WebSocket] = {}
|
||||||
|
self.visitor_connections: dict[int, WebSocket] = {}
|
||||||
|
|
||||||
|
async def connect_chat(self, websocket: WebSocket, chat_id: int, user_type: str):
|
||||||
|
await websocket.accept()
|
||||||
|
if chat_id not in self.active_connections:
|
||||||
|
self.active_connections[chat_id] = []
|
||||||
|
self.active_connections[chat_id].append(websocket)
|
||||||
|
if user_type == 'staff':
|
||||||
|
pass
|
||||||
|
elif user_type == 'visitor':
|
||||||
|
self.visitor_connections[chat_id] = websocket
|
||||||
|
|
||||||
|
def disconnect_chat(self, websocket: WebSocket, chat_id: int):
|
||||||
|
if chat_id in self.active_connections:
|
||||||
|
if websocket in self.active_connections[chat_id]:
|
||||||
|
self.active_connections[chat_id].remove(websocket)
|
||||||
|
if not self.active_connections[chat_id]:
|
||||||
|
del self.active_connections[chat_id]
|
||||||
|
if chat_id in self.visitor_connections and self.visitor_connections[chat_id] == websocket:
|
||||||
|
del self.visitor_connections[chat_id]
|
||||||
|
|
||||||
|
async def send_personal_message(self, message: dict, websocket: WebSocket):
|
||||||
|
try:
|
||||||
|
await websocket.send_json(message)
|
||||||
|
except Exception as e:
|
||||||
|
print(f'Error sending message: {e}')
|
||||||
|
|
||||||
|
async def broadcast_to_chat(self, message: dict, chat_id: int):
|
||||||
|
if chat_id in self.active_connections:
|
||||||
|
disconnected = []
|
||||||
|
for connection in self.active_connections[chat_id]:
|
||||||
|
try:
|
||||||
|
await connection.send_json(message)
|
||||||
|
except Exception as e:
|
||||||
|
print(f'Error broadcasting to connection: {e}')
|
||||||
|
disconnected.append(connection)
|
||||||
|
for conn in disconnected:
|
||||||
|
self.active_connections[chat_id].remove(conn)
|
||||||
|
|
||||||
|
async def notify_staff_new_chat(self, chat_data: dict):
|
||||||
|
disconnected = []
|
||||||
|
for user_id, websocket in self.staff_connections.items():
|
||||||
|
try:
|
||||||
|
await websocket.send_json({'type': 'new_chat', 'data': chat_data})
|
||||||
|
except Exception as e:
|
||||||
|
print(f'Error notifying staff {user_id}: {e}')
|
||||||
|
disconnected.append(user_id)
|
||||||
|
for user_id in disconnected:
|
||||||
|
del self.staff_connections[user_id]
|
||||||
|
|
||||||
|
async def notify_staff_new_message(self, chat_id: int, message_data: dict, chat: Chat):
|
||||||
|
if message_data.get('sender_type') == 'visitor':
|
||||||
|
notification_data = {'type': 'new_message_notification', 'data': {'chat_id': chat_id, 'chat': {'id': chat.id, 'visitor_name': chat.visitor_name, 'visitor_email': chat.visitor_email, 'status': chat.status.value, 'created_at': chat.created_at.isoformat()}, 'message': {'id': message_data.get('id'), 'message': message_data.get('message'), 'sender_type': message_data.get('sender_type'), 'created_at': message_data.get('created_at')}}}
|
||||||
|
disconnected = []
|
||||||
|
for user_id, websocket in self.staff_connections.items():
|
||||||
|
try:
|
||||||
|
await websocket.send_json(notification_data)
|
||||||
|
except Exception as e:
|
||||||
|
print(f'Error notifying staff {user_id}: {e}')
|
||||||
|
disconnected.append(user_id)
|
||||||
|
for user_id in disconnected:
|
||||||
|
del self.staff_connections[user_id]
|
||||||
|
|
||||||
|
def connect_staff(self, user_id: int, websocket: WebSocket):
|
||||||
|
self.staff_connections[user_id] = websocket
|
||||||
|
|
||||||
|
def disconnect_staff(self, user_id: int):
|
||||||
|
if user_id in self.staff_connections:
|
||||||
|
del self.staff_connections[user_id]
|
||||||
|
manager = ConnectionManager()
|
||||||
|
|
||||||
|
@router.post('/create', status_code=status.HTTP_201_CREATED)
|
||||||
|
async def create_chat(visitor_name: Optional[str]=None, visitor_email: Optional[str]=None, visitor_phone: Optional[str]=None, current_user: Optional[User]=Depends(get_current_user_optional), db: Session=Depends(get_db)):
|
||||||
|
if current_user:
|
||||||
|
chat = Chat(visitor_id=current_user.id, visitor_name=current_user.full_name, visitor_email=current_user.email, status=ChatStatus.pending)
|
||||||
|
else:
|
||||||
|
if not visitor_name:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Visitor name is required')
|
||||||
|
if not visitor_email:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Visitor email is required')
|
||||||
|
chat = Chat(visitor_name=visitor_name, visitor_email=visitor_email, status=ChatStatus.pending)
|
||||||
|
db.add(chat)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(chat)
|
||||||
|
chat_data = {'id': chat.id, 'visitor_name': chat.visitor_name, 'visitor_email': chat.visitor_email, 'status': chat.status.value, 'created_at': chat.created_at.isoformat()}
|
||||||
|
await manager.notify_staff_new_chat(chat_data)
|
||||||
|
return {'success': True, 'data': {'id': chat.id, 'visitor_name': chat.visitor_name, 'visitor_email': chat.visitor_email, 'status': chat.status.value, 'created_at': chat.created_at.isoformat()}}
|
||||||
|
|
||||||
|
@router.post('/{chat_id}/accept')
|
||||||
|
async def accept_chat(chat_id: int, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||||
|
if current_user.role.name not in ['staff', 'admin']:
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Only staff members can accept chats')
|
||||||
|
chat = db.query(Chat).filter(Chat.id == chat_id).first()
|
||||||
|
if not chat:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Chat not found')
|
||||||
|
if chat.status != ChatStatus.pending:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Chat is not pending')
|
||||||
|
chat.staff_id = current_user.id
|
||||||
|
chat.status = ChatStatus.active
|
||||||
|
db.commit()
|
||||||
|
db.refresh(chat)
|
||||||
|
await manager.broadcast_to_chat({'type': 'chat_accepted', 'data': {'staff_name': current_user.full_name, 'staff_id': current_user.id}}, chat_id)
|
||||||
|
return {'success': True, 'data': {'id': chat.id, 'staff_id': chat.staff_id, 'staff_name': current_user.full_name, 'status': chat.status.value}}
|
||||||
|
|
||||||
|
@router.get('/list')
|
||||||
|
async def list_chats(status_filter: Optional[str]=None, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||||
|
if current_user.role.name in ['staff', 'admin']:
|
||||||
|
query = db.query(Chat)
|
||||||
|
if status_filter:
|
||||||
|
try:
|
||||||
|
status_enum = ChatStatus(status_filter)
|
||||||
|
query = query.filter(Chat.status == status_enum)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
chats = query.order_by(Chat.created_at.desc()).all()
|
||||||
|
else:
|
||||||
|
chats = db.query(Chat).filter(Chat.visitor_id == current_user.id).order_by(Chat.created_at.desc()).all()
|
||||||
|
return {'success': True, 'data': [{'id': chat.id, 'visitor_id': chat.visitor_id, 'visitor_name': chat.visitor_name, 'visitor_email': chat.visitor_email, 'staff_id': chat.staff_id, 'staff_name': chat.staff.full_name if chat.staff else None, 'status': chat.status.value, 'created_at': chat.created_at.isoformat(), 'updated_at': chat.updated_at.isoformat(), 'message_count': len(chat.messages)} for chat in chats]}
|
||||||
|
|
||||||
|
@router.get('/{chat_id}')
|
||||||
|
async def get_chat(chat_id: int, current_user: Optional[User]=Depends(get_current_user_optional), db: Session=Depends(get_db)):
|
||||||
|
chat = db.query(Chat).filter(Chat.id == chat_id).first()
|
||||||
|
if not chat:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Chat not found')
|
||||||
|
if current_user:
|
||||||
|
if current_user.role.name not in ['staff', 'admin']:
|
||||||
|
if chat.visitor_id != current_user.id:
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You don't have permission to view this chat")
|
||||||
|
return {'success': True, 'data': {'id': chat.id, 'visitor_id': chat.visitor_id, 'visitor_name': chat.visitor_name, 'visitor_email': chat.visitor_email, 'staff_id': chat.staff_id, 'staff_name': chat.staff.full_name if chat.staff else None, 'status': chat.status.value, 'created_at': chat.created_at.isoformat(), 'updated_at': chat.updated_at.isoformat()}}
|
||||||
|
|
||||||
|
@router.get('/{chat_id}/messages')
|
||||||
|
async def get_messages(chat_id: int, current_user: Optional[User]=Depends(get_current_user_optional), db: Session=Depends(get_db)):
|
||||||
|
chat = db.query(Chat).filter(Chat.id == chat_id).first()
|
||||||
|
if not chat:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Chat not found')
|
||||||
|
if current_user:
|
||||||
|
if current_user.role.name not in ['staff', 'admin']:
|
||||||
|
if chat.visitor_id != current_user.id:
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You don't have permission to view this chat")
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
messages = db.query(ChatMessage).filter(ChatMessage.chat_id == chat_id).order_by(ChatMessage.created_at.asc()).all()
|
||||||
|
return {'success': True, 'data': [{'id': msg.id, 'chat_id': msg.chat_id, 'sender_id': msg.sender_id, 'sender_type': msg.sender_type, 'sender_name': msg.sender.full_name if msg.sender else None, 'message': msg.message, 'is_read': msg.is_read, 'created_at': msg.created_at.isoformat()} for msg in messages]}
|
||||||
|
|
||||||
|
@router.post('/{chat_id}/message')
|
||||||
|
async def send_message(chat_id: int, message: str, current_user: Optional[User]=Depends(get_current_user_optional), db: Session=Depends(get_db)):
|
||||||
|
chat = db.query(Chat).filter(Chat.id == chat_id).first()
|
||||||
|
if not chat:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Chat not found')
|
||||||
|
if chat.status == ChatStatus.closed:
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='Chat is closed')
|
||||||
|
sender_type = 'visitor'
|
||||||
|
sender_id = None
|
||||||
|
if current_user:
|
||||||
|
if current_user.role.name in ['staff', 'admin']:
|
||||||
|
sender_type = 'staff'
|
||||||
|
sender_id = current_user.id
|
||||||
|
else:
|
||||||
|
sender_type = 'visitor'
|
||||||
|
sender_id = current_user.id
|
||||||
|
if chat.visitor_id != current_user.id:
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You don't have permission to send messages in this chat")
|
||||||
|
else:
|
||||||
|
sender_type = 'visitor'
|
||||||
|
sender_id = None
|
||||||
|
chat_message = ChatMessage(chat_id=chat_id, sender_id=sender_id, sender_type=sender_type, message=message)
|
||||||
|
db.add(chat_message)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(chat_message)
|
||||||
|
message_data = {'type': 'new_message', 'data': {'id': chat_message.id, 'chat_id': chat_message.chat_id, 'sender_id': chat_message.sender_id, 'sender_type': chat_message.sender_type, 'sender_name': chat_message.sender.full_name if chat_message.sender else None, 'message': chat_message.message, 'is_read': chat_message.is_read, 'created_at': chat_message.created_at.isoformat()}}
|
||||||
|
await manager.broadcast_to_chat(message_data, chat_id)
|
||||||
|
if chat_message.sender_type == 'visitor':
|
||||||
|
await manager.notify_staff_new_message(chat_id, message_data['data'], chat)
|
||||||
|
return {'success': True, 'data': {'id': chat_message.id, 'chat_id': chat_message.chat_id, 'sender_type': chat_message.sender_type, 'message': chat_message.message, 'created_at': chat_message.created_at.isoformat()}}
|
||||||
|
|
||||||
|
@router.post('/{chat_id}/close')
|
||||||
|
async def close_chat(chat_id: int, current_user: Optional[User]=Depends(get_current_user_optional), db: Session=Depends(get_db)):
|
||||||
|
chat = db.query(Chat).filter(Chat.id == chat_id).first()
|
||||||
|
if not chat:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Chat not found')
|
||||||
|
if current_user:
|
||||||
|
if current_user.role.name not in ['staff', 'admin']:
|
||||||
|
if chat.visitor_id != current_user.id:
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You don't have permission to close this chat")
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
chat.status = ChatStatus.closed
|
||||||
|
chat.closed_at = datetime.utcnow()
|
||||||
|
db.commit()
|
||||||
|
await manager.broadcast_to_chat({'type': 'chat_closed', 'data': {'chat_id': chat_id}}, chat_id)
|
||||||
|
return {'success': True, 'data': {'id': chat.id, 'status': chat.status.value}}
|
||||||
|
|
||||||
|
@router.websocket('/ws/{chat_id}')
|
||||||
|
async def websocket_chat(websocket: WebSocket, chat_id: int, user_type: str=None, token: Optional[str]=None):
|
||||||
|
query_params = dict(websocket.query_params)
|
||||||
|
user_type = query_params.get('user_type', 'visitor')
|
||||||
|
token = query_params.get('token')
|
||||||
|
current_user = None
|
||||||
|
if user_type == 'staff' and token:
|
||||||
|
try:
|
||||||
|
from ..middleware.auth import verify_token
|
||||||
|
from ..config.database import get_db
|
||||||
|
payload = verify_token(token)
|
||||||
|
user_id = payload.get('userId')
|
||||||
|
db_gen = get_db()
|
||||||
|
db = next(db_gen)
|
||||||
|
try:
|
||||||
|
current_user = db.query(User).filter(User.id == user_id).first()
|
||||||
|
if not current_user or current_user.role.name not in ['staff', 'admin']:
|
||||||
|
await websocket.close(code=1008, reason='Unauthorized')
|
||||||
|
return
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
except Exception as e:
|
||||||
|
await websocket.close(code=1008, reason='Invalid token')
|
||||||
|
return
|
||||||
|
await manager.connect_chat(websocket, chat_id, user_type)
|
||||||
|
if user_type == 'staff' and current_user:
|
||||||
|
manager.connect_staff(current_user.id, websocket)
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
data = await websocket.receive_text()
|
||||||
|
message_data = json.loads(data)
|
||||||
|
if message_data.get('type') == 'message':
|
||||||
|
from ..config.database import get_db
|
||||||
|
db_gen = get_db()
|
||||||
|
db = next(db_gen)
|
||||||
|
try:
|
||||||
|
chat = db.query(Chat).filter(Chat.id == chat_id).first()
|
||||||
|
if not chat:
|
||||||
|
continue
|
||||||
|
sender_id = current_user.id if current_user else None
|
||||||
|
sender_type = 'staff' if user_type == 'staff' else 'visitor'
|
||||||
|
chat_message = ChatMessage(chat_id=chat_id, sender_id=sender_id, sender_type=sender_type, message=message_data.get('message', ''))
|
||||||
|
db.add(chat_message)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(chat_message)
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
message_data = {'type': 'new_message', 'data': {'id': chat_message.id, 'chat_id': chat_message.chat_id, 'sender_id': chat_message.sender_id, 'sender_type': chat_message.sender_type, 'sender_name': chat_message.sender.full_name if chat_message.sender else None, 'message': chat_message.message, 'is_read': chat_message.is_read, 'created_at': chat_message.created_at.isoformat()}}
|
||||||
|
await manager.broadcast_to_chat(message_data, chat_id)
|
||||||
|
if chat_message.sender_type == 'visitor':
|
||||||
|
await manager.notify_staff_new_message(chat_id, message_data['data'], chat)
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
manager.disconnect_chat(websocket, chat_id)
|
||||||
|
if user_type == 'staff' and current_user:
|
||||||
|
manager.disconnect_staff(current_user.id)
|
||||||
|
|
||||||
|
@router.websocket('/ws/staff/notifications')
|
||||||
|
async def websocket_staff_notifications(websocket: WebSocket):
|
||||||
|
current_user = None
|
||||||
|
try:
|
||||||
|
await websocket.accept()
|
||||||
|
query_params = dict(websocket.query_params)
|
||||||
|
token = query_params.get('token')
|
||||||
|
if not token:
|
||||||
|
await websocket.close(code=1008, reason='Token required')
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
from ..middleware.auth import verify_token
|
||||||
|
from ..config.database import get_db
|
||||||
|
payload = verify_token(token)
|
||||||
|
user_id = payload.get('userId')
|
||||||
|
if not user_id:
|
||||||
|
await websocket.close(code=1008, reason='Invalid token payload')
|
||||||
|
return
|
||||||
|
db_gen = get_db()
|
||||||
|
db = next(db_gen)
|
||||||
|
try:
|
||||||
|
current_user = db.query(User).filter(User.id == user_id).first()
|
||||||
|
if not current_user:
|
||||||
|
await websocket.close(code=1008, reason='User not found')
|
||||||
|
return
|
||||||
|
role = db.query(Role).filter(Role.id == current_user.role_id).first()
|
||||||
|
if not role or role.name not in ['staff', 'admin']:
|
||||||
|
await websocket.close(code=1008, reason='Unauthorized role')
|
||||||
|
return
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f'WebSocket token verification error: {e}')
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
await websocket.close(code=1008, reason=f'Token verification failed: {str(e)}')
|
||||||
|
return
|
||||||
|
manager.connect_staff(current_user.id, websocket)
|
||||||
|
try:
|
||||||
|
await websocket.send_json({'type': 'connected', 'data': {'message': 'WebSocket connected'}})
|
||||||
|
except Exception as e:
|
||||||
|
print(f'Error sending initial message: {e}')
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
data = await websocket.receive_text()
|
||||||
|
try:
|
||||||
|
message_data = json.loads(data)
|
||||||
|
if message_data.get('type') == 'ping':
|
||||||
|
await websocket.send_json({'type': 'pong', 'data': 'pong'})
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
await websocket.send_json({'type': 'pong', 'data': 'pong'})
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
print('WebSocket disconnected normally')
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f'WebSocket receive error: {e}')
|
||||||
|
break
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
print('WebSocket disconnected')
|
||||||
|
except Exception as e:
|
||||||
|
print(f'WebSocket error: {e}')
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
finally:
|
||||||
|
if current_user:
|
||||||
|
try:
|
||||||
|
manager.disconnect_staff(current_user.id)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
@@ -1,68 +1,23 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from ..config.database import get_db
|
from ..config.database import get_db
|
||||||
from ..config.logging_config import get_logger
|
from ..config.logging_config import get_logger
|
||||||
from ..models.page_content import PageContent, PageType
|
from ..models.page_content import PageContent, PageType
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
router = APIRouter(prefix='/contact-content', tags=['contact-content'])
|
||||||
router = APIRouter(prefix="/contact-content", tags=["contact-content"])
|
|
||||||
|
|
||||||
|
|
||||||
def serialize_page_content(content: PageContent) -> dict:
|
def serialize_page_content(content: PageContent) -> dict:
|
||||||
"""Serialize PageContent model to dictionary"""
|
return {'id': content.id, 'page_type': content.page_type.value, 'title': content.title, 'subtitle': content.subtitle, 'description': content.description, 'content': content.content, 'meta_title': content.meta_title, 'meta_description': content.meta_description, 'meta_keywords': content.meta_keywords, 'og_title': content.og_title, 'og_description': content.og_description, 'og_image': content.og_image, 'canonical_url': content.canonical_url, 'contact_info': json.loads(content.contact_info) if content.contact_info else None, 'map_url': content.map_url, 'is_active': content.is_active, 'created_at': content.created_at.isoformat() if content.created_at else None, 'updated_at': content.updated_at.isoformat() if content.updated_at else None}
|
||||||
return {
|
|
||||||
"id": content.id,
|
|
||||||
"page_type": content.page_type.value,
|
|
||||||
"title": content.title,
|
|
||||||
"subtitle": content.subtitle,
|
|
||||||
"description": content.description,
|
|
||||||
"content": content.content,
|
|
||||||
"meta_title": content.meta_title,
|
|
||||||
"meta_description": content.meta_description,
|
|
||||||
"meta_keywords": content.meta_keywords,
|
|
||||||
"og_title": content.og_title,
|
|
||||||
"og_description": content.og_description,
|
|
||||||
"og_image": content.og_image,
|
|
||||||
"canonical_url": content.canonical_url,
|
|
||||||
"contact_info": json.loads(content.contact_info) if content.contact_info else None,
|
|
||||||
"map_url": content.map_url,
|
|
||||||
"is_active": content.is_active,
|
|
||||||
"created_at": content.created_at.isoformat() if content.created_at else None,
|
|
||||||
"updated_at": content.updated_at.isoformat() if content.updated_at else None,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@router.get('/')
|
||||||
@router.get("/")
|
async def get_contact_content(db: Session=Depends(get_db)):
|
||||||
async def get_contact_content(
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Get contact page content"""
|
|
||||||
try:
|
try:
|
||||||
content = db.query(PageContent).filter(PageContent.page_type == PageType.CONTACT).first()
|
content = db.query(PageContent).filter(PageContent.page_type == PageType.CONTACT).first()
|
||||||
|
|
||||||
if not content:
|
if not content:
|
||||||
return {
|
return {'status': 'success', 'data': {'page_content': None}}
|
||||||
"status": "success",
|
|
||||||
"data": {
|
|
||||||
"page_content": None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
content_dict = serialize_page_content(content)
|
content_dict = serialize_page_content(content)
|
||||||
|
return {'status': 'success', 'data': {'page_content': content_dict}}
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"data": {
|
|
||||||
"page_content": content_dict
|
|
||||||
}
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error fetching contact content: {str(e)}", exc_info=True)
|
logger.error(f'Error fetching contact content: {str(e)}', exc_info=True)
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error fetching contact content: {str(e)}')
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
detail=f"Error fetching contact content: {str(e)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
@@ -3,17 +3,13 @@ from sqlalchemy.orm import Session
|
|||||||
from pydantic import BaseModel, EmailStr
|
from pydantic import BaseModel, EmailStr
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from ..config.database import get_db
|
from ..config.database import get_db
|
||||||
from ..models.user import User
|
from ..models.user import User
|
||||||
from ..models.role import Role
|
from ..models.role import Role
|
||||||
from ..models.system_settings import SystemSettings
|
from ..models.system_settings import SystemSettings
|
||||||
from ..utils.mailer import send_email
|
from ..utils.mailer import send_email
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
router = APIRouter(prefix='/contact', tags=['contact'])
|
||||||
router = APIRouter(prefix="/contact", tags=["contact"])
|
|
||||||
|
|
||||||
|
|
||||||
class ContactForm(BaseModel):
|
class ContactForm(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
@@ -22,182 +18,35 @@ class ContactForm(BaseModel):
|
|||||||
message: str
|
message: str
|
||||||
phone: Optional[str] = None
|
phone: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
def get_admin_email(db: Session) -> str:
|
def get_admin_email(db: Session) -> str:
|
||||||
"""Get admin email from system settings or find admin user"""
|
company_email_setting = db.query(SystemSettings).filter(SystemSettings.key == 'company_email').first()
|
||||||
# First, try to get from company_email (company settings)
|
|
||||||
company_email_setting = db.query(SystemSettings).filter(
|
|
||||||
SystemSettings.key == "company_email"
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if company_email_setting and company_email_setting.value:
|
if company_email_setting and company_email_setting.value:
|
||||||
return company_email_setting.value
|
return company_email_setting.value
|
||||||
|
admin_email_setting = db.query(SystemSettings).filter(SystemSettings.key == 'admin_email').first()
|
||||||
# Second, try to get from admin_email (legacy setting)
|
|
||||||
admin_email_setting = db.query(SystemSettings).filter(
|
|
||||||
SystemSettings.key == "admin_email"
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if admin_email_setting and admin_email_setting.value:
|
if admin_email_setting and admin_email_setting.value:
|
||||||
return admin_email_setting.value
|
return admin_email_setting.value
|
||||||
|
admin_role = db.query(Role).filter(Role.name == 'admin').first()
|
||||||
# If not found in settings, find the first admin user
|
|
||||||
admin_role = db.query(Role).filter(Role.name == "admin").first()
|
|
||||||
if admin_role:
|
if admin_role:
|
||||||
admin_user = db.query(User).filter(
|
admin_user = db.query(User).filter(User.role_id == admin_role.id, User.is_active == True).first()
|
||||||
User.role_id == admin_role.id,
|
|
||||||
User.is_active == True
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if admin_user:
|
if admin_user:
|
||||||
return admin_user.email
|
return admin_user.email
|
||||||
|
|
||||||
# Fallback to SMTP_FROM_EMAIL if configured
|
|
||||||
from ..config.settings import settings
|
from ..config.settings import settings
|
||||||
if settings.SMTP_FROM_EMAIL:
|
if settings.SMTP_FROM_EMAIL:
|
||||||
return settings.SMTP_FROM_EMAIL
|
return settings.SMTP_FROM_EMAIL
|
||||||
|
raise HTTPException(status_code=500, detail='Admin email not configured. Please set company_email in system settings or ensure an admin user exists.')
|
||||||
# Last resort: raise error
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=500,
|
|
||||||
detail="Admin email not configured. Please set company_email in system settings or ensure an admin user exists."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
@router.post('/submit')
|
||||||
@router.post("/submit")
|
async def submit_contact_form(contact_data: ContactForm, db: Session=Depends(get_db)):
|
||||||
async def submit_contact_form(
|
|
||||||
contact_data: ContactForm,
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Submit contact form and send email to admin"""
|
|
||||||
try:
|
try:
|
||||||
# Get admin email
|
|
||||||
admin_email = get_admin_email(db)
|
admin_email = get_admin_email(db)
|
||||||
|
subject = f'Contact Form: {contact_data.subject}'
|
||||||
# Create email subject
|
html_body = f
|
||||||
subject = f"Contact Form: {contact_data.subject}"
|
text_body = f
|
||||||
|
await send_email(to=admin_email, subject=subject, html=html_body, text=text_body)
|
||||||
# Create email body (HTML)
|
logger.info(f'Contact form submitted successfully. Email sent to {admin_email}')
|
||||||
html_body = f"""
|
return {'status': 'success', 'message': 'Thank you for contacting us! We will get back to you soon.'}
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<style>
|
|
||||||
body {{
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #333;
|
|
||||||
}}
|
|
||||||
.container {{
|
|
||||||
max-width: 600px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}}
|
|
||||||
.header {{
|
|
||||||
background: linear-gradient(135deg, #d4af37 0%, #c9a227 100%);
|
|
||||||
color: #0f0f0f;
|
|
||||||
padding: 20px;
|
|
||||||
text-align: center;
|
|
||||||
border-radius: 8px 8px 0 0;
|
|
||||||
}}
|
|
||||||
.content {{
|
|
||||||
background-color: #ffffff;
|
|
||||||
padding: 30px;
|
|
||||||
border-radius: 0 0 8px 8px;
|
|
||||||
}}
|
|
||||||
.field {{
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}}
|
|
||||||
.label {{
|
|
||||||
font-weight: bold;
|
|
||||||
color: #d4af37;
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}}
|
|
||||||
.value {{
|
|
||||||
color: #333;
|
|
||||||
padding: 10px;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
border-radius: 4px;
|
|
||||||
}}
|
|
||||||
.footer {{
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 20px;
|
|
||||||
padding-top: 20px;
|
|
||||||
border-top: 1px solid #ddd;
|
|
||||||
color: #666;
|
|
||||||
font-size: 12px;
|
|
||||||
}}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<div class="header">
|
|
||||||
<h2>New Contact Form Submission</h2>
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<div class="field">
|
|
||||||
<span class="label">Name:</span>
|
|
||||||
<div class="value">{contact_data.name}</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<span class="label">Email:</span>
|
|
||||||
<div class="value">{contact_data.email}</div>
|
|
||||||
</div>
|
|
||||||
{f'<div class="field"><span class="label">Phone:</span><div class="value">{contact_data.phone}</div></div>' if contact_data.phone else ''}
|
|
||||||
<div class="field">
|
|
||||||
<span class="label">Subject:</span>
|
|
||||||
<div class="value">{contact_data.subject}</div>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<span class="label">Message:</span>
|
|
||||||
<div class="value" style="white-space: pre-wrap;">{contact_data.message}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="footer">
|
|
||||||
<p>This email was sent from the hotel booking contact form.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Create plain text version
|
|
||||||
text_body = f"""
|
|
||||||
New Contact Form Submission
|
|
||||||
|
|
||||||
Name: {contact_data.name}
|
|
||||||
Email: {contact_data.email}
|
|
||||||
{f'Phone: {contact_data.phone}' if contact_data.phone else ''}
|
|
||||||
Subject: {contact_data.subject}
|
|
||||||
|
|
||||||
Message:
|
|
||||||
{contact_data.message}
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Send email to admin
|
|
||||||
await send_email(
|
|
||||||
to=admin_email,
|
|
||||||
subject=subject,
|
|
||||||
html=html_body,
|
|
||||||
text=text_body
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(f"Contact form submitted successfully. Email sent to {admin_email}")
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"message": "Thank you for contacting us! We will get back to you soon."
|
|
||||||
}
|
|
||||||
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to submit contact form: {type(e).__name__}: {str(e)}", exc_info=True)
|
logger.error(f'Failed to submit contact form: {type(e).__name__}: {str(e)}', exc_info=True)
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=500, detail='Failed to submit contact form. Please try again later.')
|
||||||
status_code=500,
|
|
||||||
detail="Failed to submit contact form. Please try again later."
|
|
||||||
)
|
|
||||||
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
|
|
||||||
from ..config.database import get_db
|
from ..config.database import get_db
|
||||||
from ..middleware.auth import get_current_user
|
from ..middleware.auth import get_current_user
|
||||||
from ..models.user import User
|
from ..models.user import User
|
||||||
@@ -9,179 +8,74 @@ from ..models.favorite import Favorite
|
|||||||
from ..models.room import Room
|
from ..models.room import Room
|
||||||
from ..models.room_type import RoomType
|
from ..models.room_type import RoomType
|
||||||
from ..models.review import Review, ReviewStatus
|
from ..models.review import Review, ReviewStatus
|
||||||
|
router = APIRouter(prefix='/favorites', tags=['favorites'])
|
||||||
|
|
||||||
router = APIRouter(prefix="/favorites", tags=["favorites"])
|
@router.get('/')
|
||||||
|
async def get_favorites(current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||||
|
if current_user.role in ['admin', 'staff']:
|
||||||
@router.get("/")
|
raise HTTPException(status_code=403, detail='Admin and staff users cannot have favorites')
|
||||||
async def get_favorites(
|
|
||||||
current_user: User = Depends(get_current_user),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Get user's favorite rooms"""
|
|
||||||
try:
|
try:
|
||||||
favorites = db.query(Favorite).filter(
|
favorites = db.query(Favorite).filter(Favorite.user_id == current_user.id).order_by(Favorite.created_at.desc()).all()
|
||||||
Favorite.user_id == current_user.id
|
|
||||||
).order_by(Favorite.created_at.desc()).all()
|
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for favorite in favorites:
|
for favorite in favorites:
|
||||||
if not favorite.room:
|
if not favorite.room:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
room = favorite.room
|
room = favorite.room
|
||||||
|
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()
|
||||||
# Get review stats
|
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}
|
||||||
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:
|
if room.room_type:
|
||||||
room_dict["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}
|
||||||
"id": room.room_type.id,
|
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}
|
||||||
"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)
|
result.append(favorite_dict)
|
||||||
|
return {'status': 'success', 'data': {'favorites': result, 'total': len(result)}}
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"data": {
|
|
||||||
"favorites": result,
|
|
||||||
"total": len(result),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.post('/{room_id}')
|
||||||
@router.post("/{room_id}")
|
async def add_favorite(room_id: int, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||||
async def add_favorite(
|
if current_user.role in ['admin', 'staff']:
|
||||||
room_id: int,
|
raise HTTPException(status_code=403, detail='Admin and staff users cannot add favorites')
|
||||||
current_user: User = Depends(get_current_user),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Add room to favorites"""
|
|
||||||
try:
|
try:
|
||||||
# Check if room exists
|
|
||||||
room = db.query(Room).filter(Room.id == room_id).first()
|
room = db.query(Room).filter(Room.id == room_id).first()
|
||||||
if not room:
|
if not room:
|
||||||
raise HTTPException(status_code=404, detail="Room not found")
|
raise HTTPException(status_code=404, detail='Room not found')
|
||||||
|
existing = db.query(Favorite).filter(Favorite.user_id == current_user.id, Favorite.room_id == room_id).first()
|
||||||
# Check if already favorited
|
|
||||||
existing = db.query(Favorite).filter(
|
|
||||||
Favorite.user_id == current_user.id,
|
|
||||||
Favorite.room_id == room_id
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=400, detail='Room already in favorites list')
|
||||||
status_code=400,
|
favorite = Favorite(user_id=current_user.id, room_id=room_id)
|
||||||
detail="Room already in favorites list"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create favorite
|
|
||||||
favorite = Favorite(
|
|
||||||
user_id=current_user.id,
|
|
||||||
room_id=room_id
|
|
||||||
)
|
|
||||||
|
|
||||||
db.add(favorite)
|
db.add(favorite)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(favorite)
|
db.refresh(favorite)
|
||||||
|
return {'status': 'success', 'message': 'Added to favorites list', 'data': {'favorite': favorite}}
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"message": "Added to favorites list",
|
|
||||||
"data": {"favorite": favorite}
|
|
||||||
}
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.delete('/{room_id}')
|
||||||
@router.delete("/{room_id}")
|
async def remove_favorite(room_id: int, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||||
async def remove_favorite(
|
if current_user.role in ['admin', 'staff']:
|
||||||
room_id: int,
|
raise HTTPException(status_code=403, detail='Admin and staff users cannot remove favorites')
|
||||||
current_user: User = Depends(get_current_user),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Remove room from favorites"""
|
|
||||||
try:
|
try:
|
||||||
favorite = db.query(Favorite).filter(
|
favorite = db.query(Favorite).filter(Favorite.user_id == current_user.id, Favorite.room_id == room_id).first()
|
||||||
Favorite.user_id == current_user.id,
|
|
||||||
Favorite.room_id == room_id
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if not favorite:
|
if not favorite:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=404, detail='Room not found in favorites list')
|
||||||
status_code=404,
|
|
||||||
detail="Room not found in favorites list"
|
|
||||||
)
|
|
||||||
|
|
||||||
db.delete(favorite)
|
db.delete(favorite)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
return {'status': 'success', 'message': 'Removed from favorites list'}
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"message": "Removed from favorites list"
|
|
||||||
}
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.get('/check/{room_id}')
|
||||||
@router.get("/check/{room_id}")
|
async def check_favorite(room_id: int, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||||
async def check_favorite(
|
if current_user.role in ['admin', 'staff']:
|
||||||
room_id: int,
|
return {'status': 'success', 'data': {'isFavorited': False}}
|
||||||
current_user: User = Depends(get_current_user),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Check if room is favorited by user"""
|
|
||||||
try:
|
try:
|
||||||
favorite = db.query(Favorite).filter(
|
favorite = db.query(Favorite).filter(Favorite.user_id == current_user.id, Favorite.room_id == room_id).first()
|
||||||
Favorite.user_id == current_user.id,
|
return {'status': 'success', 'data': {'isFavorited': favorite is not None}}
|
||||||
Favorite.room_id == room_id
|
|
||||||
).first()
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"data": {"isFavorited": favorite is not None}
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
@@ -1,63 +1,23 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from ..config.database import get_db
|
from ..config.database import get_db
|
||||||
from ..config.logging_config import get_logger
|
from ..config.logging_config import get_logger
|
||||||
from ..models.page_content import PageContent, PageType
|
from ..models.page_content import PageContent, PageType
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
router = APIRouter(prefix='/footer', tags=['footer'])
|
||||||
router = APIRouter(prefix="/footer", tags=["footer"])
|
|
||||||
|
|
||||||
|
|
||||||
def serialize_page_content(content: PageContent) -> dict:
|
def serialize_page_content(content: PageContent) -> dict:
|
||||||
"""Serialize PageContent model to dictionary"""
|
return {'id': content.id, 'page_type': content.page_type.value, 'title': content.title, 'subtitle': content.subtitle, 'description': content.description, 'content': content.content, 'social_links': json.loads(content.social_links) if content.social_links else None, 'footer_links': json.loads(content.footer_links) if content.footer_links else None, 'badges': json.loads(content.badges) if content.badges else None, 'copyright_text': content.copyright_text, 'is_active': content.is_active, 'created_at': content.created_at.isoformat() if content.created_at else None, 'updated_at': content.updated_at.isoformat() if content.updated_at else None}
|
||||||
return {
|
|
||||||
"id": content.id,
|
|
||||||
"page_type": content.page_type.value,
|
|
||||||
"title": content.title,
|
|
||||||
"subtitle": content.subtitle,
|
|
||||||
"description": content.description,
|
|
||||||
"content": content.content,
|
|
||||||
"social_links": json.loads(content.social_links) if content.social_links else None,
|
|
||||||
"footer_links": json.loads(content.footer_links) if content.footer_links else None,
|
|
||||||
"badges": json.loads(content.badges) if content.badges else None,
|
|
||||||
"copyright_text": content.copyright_text,
|
|
||||||
"is_active": content.is_active,
|
|
||||||
"created_at": content.created_at.isoformat() if content.created_at else None,
|
|
||||||
"updated_at": content.updated_at.isoformat() if content.updated_at else None,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@router.get('/')
|
||||||
@router.get("/")
|
async def get_footer_content(db: Session=Depends(get_db)):
|
||||||
async def get_footer_content(
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Get footer content"""
|
|
||||||
try:
|
try:
|
||||||
content = db.query(PageContent).filter(PageContent.page_type == PageType.FOOTER).first()
|
content = db.query(PageContent).filter(PageContent.page_type == PageType.FOOTER).first()
|
||||||
|
|
||||||
if not content:
|
if not content:
|
||||||
return {
|
return {'status': 'success', 'data': {'page_content': None}}
|
||||||
"status": "success",
|
|
||||||
"data": {
|
|
||||||
"page_content": None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
content_dict = serialize_page_content(content)
|
content_dict = serialize_page_content(content)
|
||||||
|
return {'status': 'success', 'data': {'page_content': content_dict}}
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"data": {
|
|
||||||
"page_content": content_dict
|
|
||||||
}
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error fetching footer content: {str(e)}", exc_info=True)
|
logger.error(f'Error fetching footer content: {str(e)}', exc_info=True)
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error fetching footer content: {str(e)}')
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
detail=f"Error fetching footer content: {str(e)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
@@ -1,110 +1,23 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from ..config.database import get_db
|
from ..config.database import get_db
|
||||||
from ..config.logging_config import get_logger
|
from ..config.logging_config import get_logger
|
||||||
from ..models.page_content import PageContent, PageType
|
from ..models.page_content import PageContent, PageType
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
router = APIRouter(prefix='/home', tags=['home'])
|
||||||
router = APIRouter(prefix="/home", tags=["home"])
|
|
||||||
|
|
||||||
|
|
||||||
def serialize_page_content(content: PageContent) -> dict:
|
def serialize_page_content(content: PageContent) -> dict:
|
||||||
"""Serialize PageContent model to dictionary"""
|
return {'id': content.id, 'page_type': content.page_type.value, 'title': content.title, 'subtitle': content.subtitle, 'description': content.description, 'content': content.content, 'meta_title': content.meta_title, 'meta_description': content.meta_description, 'meta_keywords': content.meta_keywords, 'og_title': content.og_title, 'og_description': content.og_description, 'og_image': content.og_image, 'canonical_url': content.canonical_url, 'hero_title': content.hero_title, 'hero_subtitle': content.hero_subtitle, 'hero_image': content.hero_image, 'amenities_section_title': content.amenities_section_title, 'amenities_section_subtitle': content.amenities_section_subtitle, 'amenities': json.loads(content.amenities) if content.amenities else None, 'testimonials_section_title': content.testimonials_section_title, 'testimonials_section_subtitle': content.testimonials_section_subtitle, 'testimonials': json.loads(content.testimonials) if content.testimonials else None, 'gallery_section_title': content.gallery_section_title, 'gallery_section_subtitle': content.gallery_section_subtitle, 'gallery_images': json.loads(content.gallery_images) if content.gallery_images else None, 'luxury_section_title': content.luxury_section_title, 'luxury_section_subtitle': content.luxury_section_subtitle, 'luxury_section_image': content.luxury_section_image, 'luxury_features': json.loads(content.luxury_features) if content.luxury_features else None, 'luxury_gallery_section_title': content.luxury_gallery_section_title, 'luxury_gallery_section_subtitle': content.luxury_gallery_section_subtitle, 'luxury_gallery': json.loads(content.luxury_gallery) if content.luxury_gallery else None, 'luxury_testimonials_section_title': content.luxury_testimonials_section_title, 'luxury_testimonials_section_subtitle': content.luxury_testimonials_section_subtitle, 'luxury_testimonials': json.loads(content.luxury_testimonials) if content.luxury_testimonials else None, 'about_preview_title': content.about_preview_title, 'about_preview_subtitle': content.about_preview_subtitle, 'about_preview_content': content.about_preview_content, 'about_preview_image': content.about_preview_image, 'stats': json.loads(content.stats) if content.stats else None, 'luxury_services_section_title': content.luxury_services_section_title, 'luxury_services_section_subtitle': content.luxury_services_section_subtitle, 'luxury_services': json.loads(content.luxury_services) if content.luxury_services else None, 'luxury_experiences_section_title': content.luxury_experiences_section_title, 'luxury_experiences_section_subtitle': content.luxury_experiences_section_subtitle, 'luxury_experiences': json.loads(content.luxury_experiences) if content.luxury_experiences else None, 'awards_section_title': content.awards_section_title, 'awards_section_subtitle': content.awards_section_subtitle, 'awards': json.loads(content.awards) if content.awards else None, 'cta_title': content.cta_title, 'cta_subtitle': content.cta_subtitle, 'cta_button_text': content.cta_button_text, 'cta_button_link': content.cta_button_link, 'cta_image': content.cta_image, 'partners_section_title': content.partners_section_title, 'partners_section_subtitle': content.partners_section_subtitle, 'partners': json.loads(content.partners) if content.partners else None, 'is_active': content.is_active, 'created_at': content.created_at.isoformat() if content.created_at else None, 'updated_at': content.updated_at.isoformat() if content.updated_at else None}
|
||||||
return {
|
|
||||||
"id": content.id,
|
|
||||||
"page_type": content.page_type.value,
|
|
||||||
"title": content.title,
|
|
||||||
"subtitle": content.subtitle,
|
|
||||||
"description": content.description,
|
|
||||||
"content": content.content,
|
|
||||||
"meta_title": content.meta_title,
|
|
||||||
"meta_description": content.meta_description,
|
|
||||||
"meta_keywords": content.meta_keywords,
|
|
||||||
"og_title": content.og_title,
|
|
||||||
"og_description": content.og_description,
|
|
||||||
"og_image": content.og_image,
|
|
||||||
"canonical_url": content.canonical_url,
|
|
||||||
"hero_title": content.hero_title,
|
|
||||||
"hero_subtitle": content.hero_subtitle,
|
|
||||||
"hero_image": content.hero_image,
|
|
||||||
"amenities_section_title": content.amenities_section_title,
|
|
||||||
"amenities_section_subtitle": content.amenities_section_subtitle,
|
|
||||||
"amenities": json.loads(content.amenities) if content.amenities else None,
|
|
||||||
"testimonials_section_title": content.testimonials_section_title,
|
|
||||||
"testimonials_section_subtitle": content.testimonials_section_subtitle,
|
|
||||||
"testimonials": json.loads(content.testimonials) if content.testimonials else None,
|
|
||||||
"gallery_section_title": content.gallery_section_title,
|
|
||||||
"gallery_section_subtitle": content.gallery_section_subtitle,
|
|
||||||
"gallery_images": json.loads(content.gallery_images) if content.gallery_images else None,
|
|
||||||
"luxury_section_title": content.luxury_section_title,
|
|
||||||
"luxury_section_subtitle": content.luxury_section_subtitle,
|
|
||||||
"luxury_section_image": content.luxury_section_image,
|
|
||||||
"luxury_features": json.loads(content.luxury_features) if content.luxury_features else None,
|
|
||||||
"luxury_gallery_section_title": content.luxury_gallery_section_title,
|
|
||||||
"luxury_gallery_section_subtitle": content.luxury_gallery_section_subtitle,
|
|
||||||
"luxury_gallery": json.loads(content.luxury_gallery) if content.luxury_gallery else None,
|
|
||||||
"luxury_testimonials_section_title": content.luxury_testimonials_section_title,
|
|
||||||
"luxury_testimonials_section_subtitle": content.luxury_testimonials_section_subtitle,
|
|
||||||
"luxury_testimonials": json.loads(content.luxury_testimonials) if content.luxury_testimonials else None,
|
|
||||||
"about_preview_title": content.about_preview_title,
|
|
||||||
"about_preview_subtitle": content.about_preview_subtitle,
|
|
||||||
"about_preview_content": content.about_preview_content,
|
|
||||||
"about_preview_image": content.about_preview_image,
|
|
||||||
"stats": json.loads(content.stats) if content.stats else None,
|
|
||||||
"luxury_services_section_title": content.luxury_services_section_title,
|
|
||||||
"luxury_services_section_subtitle": content.luxury_services_section_subtitle,
|
|
||||||
"luxury_services": json.loads(content.luxury_services) if content.luxury_services else None,
|
|
||||||
"luxury_experiences_section_title": content.luxury_experiences_section_title,
|
|
||||||
"luxury_experiences_section_subtitle": content.luxury_experiences_section_subtitle,
|
|
||||||
"luxury_experiences": json.loads(content.luxury_experiences) if content.luxury_experiences else None,
|
|
||||||
"awards_section_title": content.awards_section_title,
|
|
||||||
"awards_section_subtitle": content.awards_section_subtitle,
|
|
||||||
"awards": json.loads(content.awards) if content.awards else None,
|
|
||||||
"cta_title": content.cta_title,
|
|
||||||
"cta_subtitle": content.cta_subtitle,
|
|
||||||
"cta_button_text": content.cta_button_text,
|
|
||||||
"cta_button_link": content.cta_button_link,
|
|
||||||
"cta_image": content.cta_image,
|
|
||||||
"partners_section_title": content.partners_section_title,
|
|
||||||
"partners_section_subtitle": content.partners_section_subtitle,
|
|
||||||
"partners": json.loads(content.partners) if content.partners else None,
|
|
||||||
"is_active": content.is_active,
|
|
||||||
"created_at": content.created_at.isoformat() if content.created_at else None,
|
|
||||||
"updated_at": content.updated_at.isoformat() if content.updated_at else None,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@router.get('/')
|
||||||
@router.get("/")
|
async def get_home_content(db: Session=Depends(get_db)):
|
||||||
async def get_home_content(
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Get homepage content"""
|
|
||||||
try:
|
try:
|
||||||
content = db.query(PageContent).filter(PageContent.page_type == PageType.HOME).first()
|
content = db.query(PageContent).filter(PageContent.page_type == PageType.HOME).first()
|
||||||
|
|
||||||
if not content:
|
if not content:
|
||||||
return {
|
return {'status': 'success', 'data': {'page_content': None}}
|
||||||
"status": "success",
|
|
||||||
"data": {
|
|
||||||
"page_content": None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
content_dict = serialize_page_content(content)
|
content_dict = serialize_page_content(content)
|
||||||
|
return {'status': 'success', 'data': {'page_content': content_dict}}
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"data": {
|
|
||||||
"page_content": content_dict
|
|
||||||
}
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error fetching home content: {str(e)}", exc_info=True)
|
logger.error(f'Error fetching home content: {str(e)}', exc_info=True)
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f'Error fetching home content: {str(e)}')
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
detail=f"Error fetching home content: {str(e)}"
|
|
||||||
)
|
|
||||||
|
|
||||||
@@ -2,139 +2,60 @@ from fastapi import APIRouter, Depends, HTTPException, status, Query
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from ..config.database import get_db
|
from ..config.database import get_db
|
||||||
from ..middleware.auth import get_current_user, authorize_roles
|
from ..middleware.auth import get_current_user, authorize_roles
|
||||||
from ..models.user import User
|
from ..models.user import User
|
||||||
from ..models.invoice import Invoice, InvoiceStatus
|
from ..models.invoice import Invoice, InvoiceStatus
|
||||||
from ..models.booking import Booking
|
from ..models.booking import Booking
|
||||||
from ..services.invoice_service import InvoiceService
|
from ..services.invoice_service import InvoiceService
|
||||||
|
router = APIRouter(prefix='/invoices', tags=['invoices'])
|
||||||
|
|
||||||
router = APIRouter(prefix="/invoices", tags=["invoices"])
|
@router.get('/')
|
||||||
|
async def get_invoices(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)):
|
||||||
|
|
||||||
@router.get("/")
|
|
||||||
async def get_invoices(
|
|
||||||
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 invoices for current user (or all invoices for admin)"""
|
|
||||||
try:
|
try:
|
||||||
# Admin can see all invoices, users can only see their own
|
|
||||||
user_id = None if current_user.role_id == 1 else current_user.id
|
user_id = None if current_user.role_id == 1 else current_user.id
|
||||||
|
result = InvoiceService.get_invoices(db=db, user_id=user_id, booking_id=booking_id, status=status_filter, page=page, limit=limit)
|
||||||
result = InvoiceService.get_invoices(
|
return {'status': 'success', 'data': result}
|
||||||
db=db,
|
|
||||||
user_id=user_id,
|
|
||||||
booking_id=booking_id,
|
|
||||||
status=status_filter,
|
|
||||||
page=page,
|
|
||||||
limit=limit
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"data": result
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.get('/{id}')
|
||||||
@router.get("/{id}")
|
async def get_invoice_by_id(id: int, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||||
async def get_invoice_by_id(
|
|
||||||
id: int,
|
|
||||||
current_user: User = Depends(get_current_user),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Get invoice by ID"""
|
|
||||||
try:
|
try:
|
||||||
invoice = InvoiceService.get_invoice(id, db)
|
invoice = InvoiceService.get_invoice(id, db)
|
||||||
|
|
||||||
if not invoice:
|
if not invoice:
|
||||||
raise HTTPException(status_code=404, detail="Invoice not found")
|
raise HTTPException(status_code=404, detail='Invoice not found')
|
||||||
|
if current_user.role_id != 1 and invoice['user_id'] != current_user.id:
|
||||||
# Check access: admin can see all, users can only see their own
|
raise HTTPException(status_code=403, detail='Forbidden')
|
||||||
if current_user.role_id != 1 and invoice["user_id"] != current_user.id:
|
return {'status': 'success', 'data': {'invoice': invoice}}
|
||||||
raise HTTPException(status_code=403, detail="Forbidden")
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"data": {"invoice": invoice}
|
|
||||||
}
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.post('/')
|
||||||
@router.post("/")
|
async def create_invoice(invoice_data: dict, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||||
async def create_invoice(
|
|
||||||
invoice_data: dict,
|
|
||||||
current_user: User = Depends(get_current_user),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Create a new invoice from a booking (Admin/Staff only)"""
|
|
||||||
try:
|
try:
|
||||||
# Only admin/staff can create invoices
|
|
||||||
if current_user.role_id not in [1, 2]:
|
if current_user.role_id not in [1, 2]:
|
||||||
raise HTTPException(status_code=403, detail="Forbidden")
|
raise HTTPException(status_code=403, detail='Forbidden')
|
||||||
|
booking_id = invoice_data.get('booking_id')
|
||||||
booking_id = invoice_data.get("booking_id")
|
|
||||||
if not booking_id:
|
if not booking_id:
|
||||||
raise HTTPException(status_code=400, detail="booking_id is required")
|
raise HTTPException(status_code=400, detail='booking_id is required')
|
||||||
|
|
||||||
# Ensure booking_id is an integer
|
|
||||||
try:
|
try:
|
||||||
booking_id = int(booking_id)
|
booking_id = int(booking_id)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
raise HTTPException(status_code=400, detail="booking_id must be a valid integer")
|
raise HTTPException(status_code=400, detail='booking_id must be a valid integer')
|
||||||
|
|
||||||
# Check if booking exists
|
|
||||||
booking = db.query(Booking).filter(Booking.id == booking_id).first()
|
booking = db.query(Booking).filter(Booking.id == booking_id).first()
|
||||||
if not booking:
|
if not booking:
|
||||||
raise HTTPException(status_code=404, detail="Booking not found")
|
raise HTTPException(status_code=404, detail='Booking not found')
|
||||||
|
invoice_kwargs = {'company_name': invoice_data.get('company_name'), 'company_address': invoice_data.get('company_address'), 'company_phone': invoice_data.get('company_phone'), 'company_email': invoice_data.get('company_email'), 'company_tax_id': invoice_data.get('company_tax_id'), 'company_logo_url': invoice_data.get('company_logo_url'), 'customer_tax_id': invoice_data.get('customer_tax_id'), 'notes': invoice_data.get('notes'), 'terms_and_conditions': invoice_data.get('terms_and_conditions'), 'payment_instructions': invoice_data.get('payment_instructions')}
|
||||||
# Prepare invoice kwargs
|
invoice_notes = invoice_kwargs.get('notes', '')
|
||||||
invoice_kwargs = {
|
|
||||||
"company_name": invoice_data.get("company_name"),
|
|
||||||
"company_address": invoice_data.get("company_address"),
|
|
||||||
"company_phone": invoice_data.get("company_phone"),
|
|
||||||
"company_email": invoice_data.get("company_email"),
|
|
||||||
"company_tax_id": invoice_data.get("company_tax_id"),
|
|
||||||
"company_logo_url": invoice_data.get("company_logo_url"),
|
|
||||||
"customer_tax_id": invoice_data.get("customer_tax_id"),
|
|
||||||
"notes": invoice_data.get("notes"),
|
|
||||||
"terms_and_conditions": invoice_data.get("terms_and_conditions"),
|
|
||||||
"payment_instructions": invoice_data.get("payment_instructions"),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Add promotion code to invoice notes if present in booking
|
|
||||||
invoice_notes = invoice_kwargs.get("notes", "")
|
|
||||||
if booking.promotion_code:
|
if booking.promotion_code:
|
||||||
promotion_note = f"Promotion Code: {booking.promotion_code}"
|
promotion_note = f'Promotion Code: {booking.promotion_code}'
|
||||||
invoice_notes = f"{promotion_note}\n{invoice_notes}".strip() if invoice_notes else promotion_note
|
invoice_notes = f'{promotion_note}\n{invoice_notes}'.strip() if invoice_notes else promotion_note
|
||||||
invoice_kwargs["notes"] = invoice_notes
|
invoice_kwargs['notes'] = invoice_notes
|
||||||
|
invoice = InvoiceService.create_invoice_from_booking(booking_id=booking_id, db=db, created_by_id=current_user.id, tax_rate=invoice_data.get('tax_rate', 0.0), discount_amount=invoice_data.get('discount_amount', 0.0), due_days=invoice_data.get('due_days', 30), **invoice_kwargs)
|
||||||
# Create invoice
|
return {'status': 'success', 'message': 'Invoice created successfully', 'data': {'invoice': invoice}}
|
||||||
invoice = InvoiceService.create_invoice_from_booking(
|
|
||||||
booking_id=booking_id,
|
|
||||||
db=db,
|
|
||||||
created_by_id=current_user.id,
|
|
||||||
tax_rate=invoice_data.get("tax_rate", 0.0),
|
|
||||||
discount_amount=invoice_data.get("discount_amount", 0.0),
|
|
||||||
due_days=invoice_data.get("due_days", 30),
|
|
||||||
**invoice_kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"message": "Invoice created successfully",
|
|
||||||
"data": {"invoice": invoice}
|
|
||||||
}
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
@@ -142,33 +63,14 @@ async def create_invoice(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.put('/{id}')
|
||||||
@router.put("/{id}")
|
async def update_invoice(id: int, invoice_data: dict, current_user: User=Depends(authorize_roles('admin', 'staff')), db: Session=Depends(get_db)):
|
||||||
async def update_invoice(
|
|
||||||
id: int,
|
|
||||||
invoice_data: dict,
|
|
||||||
current_user: User = Depends(authorize_roles("admin", "staff")),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Update an invoice (Admin/Staff only)"""
|
|
||||||
try:
|
try:
|
||||||
invoice = db.query(Invoice).filter(Invoice.id == id).first()
|
invoice = db.query(Invoice).filter(Invoice.id == id).first()
|
||||||
if not invoice:
|
if not invoice:
|
||||||
raise HTTPException(status_code=404, detail="Invoice not found")
|
raise HTTPException(status_code=404, detail='Invoice not found')
|
||||||
|
updated_invoice = InvoiceService.update_invoice(invoice_id=id, db=db, updated_by_id=current_user.id, **invoice_data)
|
||||||
# Update invoice
|
return {'status': 'success', 'message': 'Invoice updated successfully', 'data': {'invoice': updated_invoice}}
|
||||||
updated_invoice = InvoiceService.update_invoice(
|
|
||||||
invoice_id=id,
|
|
||||||
db=db,
|
|
||||||
updated_by_id=current_user.id,
|
|
||||||
**invoice_data
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"message": "Invoice updated successfully",
|
|
||||||
"data": {"invoice": updated_invoice}
|
|
||||||
}
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
@@ -176,30 +78,12 @@ async def update_invoice(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.post('/{id}/mark-paid')
|
||||||
@router.post("/{id}/mark-paid")
|
async def mark_invoice_as_paid(id: int, payment_data: dict, current_user: User=Depends(authorize_roles('admin', 'staff')), db: Session=Depends(get_db)):
|
||||||
async def mark_invoice_as_paid(
|
|
||||||
id: int,
|
|
||||||
payment_data: dict,
|
|
||||||
current_user: User = Depends(authorize_roles("admin", "staff")),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Mark an invoice as paid (Admin/Staff only)"""
|
|
||||||
try:
|
try:
|
||||||
amount = payment_data.get("amount")
|
amount = payment_data.get('amount')
|
||||||
|
updated_invoice = InvoiceService.mark_invoice_as_paid(invoice_id=id, db=db, amount=amount, updated_by_id=current_user.id)
|
||||||
updated_invoice = InvoiceService.mark_invoice_as_paid(
|
return {'status': 'success', 'message': 'Invoice marked as paid successfully', 'data': {'invoice': updated_invoice}}
|
||||||
invoice_id=id,
|
|
||||||
db=db,
|
|
||||||
amount=amount,
|
|
||||||
updated_by_id=current_user.id
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"message": "Invoice marked as paid successfully",
|
|
||||||
"data": {"invoice": updated_invoice}
|
|
||||||
}
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
@@ -207,61 +91,32 @@ async def mark_invoice_as_paid(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.delete('/{id}')
|
||||||
@router.delete("/{id}")
|
async def delete_invoice(id: int, current_user: User=Depends(authorize_roles('admin')), db: Session=Depends(get_db)):
|
||||||
async def delete_invoice(
|
|
||||||
id: int,
|
|
||||||
current_user: User = Depends(authorize_roles("admin")),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Delete an invoice (Admin only)"""
|
|
||||||
try:
|
try:
|
||||||
invoice = db.query(Invoice).filter(Invoice.id == id).first()
|
invoice = db.query(Invoice).filter(Invoice.id == id).first()
|
||||||
if not invoice:
|
if not invoice:
|
||||||
raise HTTPException(status_code=404, detail="Invoice not found")
|
raise HTTPException(status_code=404, detail='Invoice not found')
|
||||||
|
|
||||||
db.delete(invoice)
|
db.delete(invoice)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
return {'status': 'success', 'message': 'Invoice deleted successfully'}
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"message": "Invoice deleted successfully"
|
|
||||||
}
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.get('/booking/{booking_id}')
|
||||||
@router.get("/booking/{booking_id}")
|
async def get_invoices_by_booking(booking_id: int, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||||
async def get_invoices_by_booking(
|
|
||||||
booking_id: int,
|
|
||||||
current_user: User = Depends(get_current_user),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Get all invoices for a specific booking"""
|
|
||||||
try:
|
try:
|
||||||
# Check if booking exists and user has access
|
|
||||||
booking = db.query(Booking).filter(Booking.id == booking_id).first()
|
booking = db.query(Booking).filter(Booking.id == booking_id).first()
|
||||||
if not booking:
|
if not booking:
|
||||||
raise HTTPException(status_code=404, detail="Booking not found")
|
raise HTTPException(status_code=404, detail='Booking not found')
|
||||||
|
|
||||||
# Check access: admin can see all, users can only see their own bookings
|
|
||||||
if current_user.role_id != 1 and booking.user_id != current_user.id:
|
if current_user.role_id != 1 and booking.user_id != current_user.id:
|
||||||
raise HTTPException(status_code=403, detail="Forbidden")
|
raise HTTPException(status_code=403, detail='Forbidden')
|
||||||
|
result = InvoiceService.get_invoices(db=db, booking_id=booking_id)
|
||||||
result = InvoiceService.get_invoices(
|
return {'status': 'success', 'data': result}
|
||||||
db=db,
|
|
||||||
booking_id=booking_id
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"data": result
|
|
||||||
}
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -1,111 +1,39 @@
|
|||||||
from fastapi import APIRouter, Depends, Request, Response, status
|
from fastapi import APIRouter, Depends, Request, Response, status
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from ..config.database import get_db
|
from ..config.database import get_db
|
||||||
from ..config.logging_config import get_logger
|
from ..config.logging_config import get_logger
|
||||||
from ..config.settings import settings
|
from ..config.settings import settings
|
||||||
from ..middleware.cookie_consent import COOKIE_CONSENT_COOKIE_NAME, _parse_consent_cookie
|
from ..middleware.cookie_consent import COOKIE_CONSENT_COOKIE_NAME, _parse_consent_cookie
|
||||||
from ..schemas.admin_privacy import PublicPrivacyConfigResponse
|
from ..schemas.admin_privacy import PublicPrivacyConfigResponse
|
||||||
from ..schemas.privacy import (
|
from ..schemas.privacy import CookieCategoryPreferences, CookieConsent, CookieConsentResponse, UpdateCookieConsentRequest
|
||||||
CookieCategoryPreferences,
|
|
||||||
CookieConsent,
|
|
||||||
CookieConsentResponse,
|
|
||||||
UpdateCookieConsentRequest,
|
|
||||||
)
|
|
||||||
from ..services.privacy_admin_service import privacy_admin_service
|
from ..services.privacy_admin_service import privacy_admin_service
|
||||||
|
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
router = APIRouter(prefix='/privacy', tags=['privacy'])
|
||||||
|
|
||||||
router = APIRouter(prefix="/privacy", tags=["privacy"])
|
@router.get('/cookie-consent', response_model=CookieConsentResponse, status_code=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/cookie-consent",
|
|
||||||
response_model=CookieConsentResponse,
|
|
||||||
status_code=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
async def get_cookie_consent(request: Request) -> CookieConsentResponse:
|
async def get_cookie_consent(request: Request) -> CookieConsentResponse:
|
||||||
"""
|
|
||||||
Return the current cookie consent preferences.
|
|
||||||
Reads from the cookie (if present) or returns default (necessary only).
|
|
||||||
"""
|
|
||||||
raw_cookie = request.cookies.get(COOKIE_CONSENT_COOKIE_NAME)
|
raw_cookie = request.cookies.get(COOKIE_CONSENT_COOKIE_NAME)
|
||||||
consent = _parse_consent_cookie(raw_cookie)
|
consent = _parse_consent_cookie(raw_cookie)
|
||||||
|
|
||||||
# Ensure necessary is always true
|
|
||||||
consent.categories.necessary = True
|
consent.categories.necessary = True
|
||||||
|
|
||||||
return CookieConsentResponse(data=consent)
|
return CookieConsentResponse(data=consent)
|
||||||
|
|
||||||
|
@router.post('/cookie-consent', response_model=CookieConsentResponse, status_code=status.HTTP_200_OK)
|
||||||
@router.post(
|
async def update_cookie_consent(request: UpdateCookieConsentRequest, response: Response) -> CookieConsentResponse:
|
||||||
"/cookie-consent",
|
existing_raw = response.headers.get('cookie')
|
||||||
response_model=CookieConsentResponse,
|
|
||||||
status_code=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
async def update_cookie_consent(
|
|
||||||
request: UpdateCookieConsentRequest, response: Response
|
|
||||||
) -> CookieConsentResponse:
|
|
||||||
"""
|
|
||||||
Update cookie consent preferences.
|
|
||||||
|
|
||||||
The 'necessary' category is controlled by the server and always true.
|
|
||||||
"""
|
|
||||||
# Build categories from existing cookie (if any) so partial updates work
|
|
||||||
existing_raw = response.headers.get("cookie") # usually empty here
|
|
||||||
# We can't reliably read cookies from the response; rely on defaults.
|
|
||||||
# For the purposes of this API, we always start from defaults and then
|
|
||||||
# override with the request payload.
|
|
||||||
categories = CookieCategoryPreferences()
|
categories = CookieCategoryPreferences()
|
||||||
|
|
||||||
if request.analytics is not None:
|
if request.analytics is not None:
|
||||||
categories.analytics = request.analytics
|
categories.analytics = request.analytics
|
||||||
if request.marketing is not None:
|
if request.marketing is not None:
|
||||||
categories.marketing = request.marketing
|
categories.marketing = request.marketing
|
||||||
if request.preferences is not None:
|
if request.preferences is not None:
|
||||||
categories.preferences = request.preferences
|
categories.preferences = request.preferences
|
||||||
|
|
||||||
# 'necessary' enforced server-side
|
|
||||||
categories.necessary = True
|
categories.necessary = True
|
||||||
|
|
||||||
consent = CookieConsent(categories=categories, has_decided=True)
|
consent = CookieConsent(categories=categories, has_decided=True)
|
||||||
|
response.set_cookie(key=COOKIE_CONSENT_COOKIE_NAME, value=consent.model_dump_json(), httponly=True, secure=settings.is_production, samesite='lax', max_age=365 * 24 * 60 * 60, path='/')
|
||||||
# Persist consent as a secure, HttpOnly cookie
|
logger.info('Cookie consent updated: analytics=%s, marketing=%s, preferences=%s', consent.categories.analytics, consent.categories.marketing, consent.categories.preferences)
|
||||||
response.set_cookie(
|
|
||||||
key=COOKIE_CONSENT_COOKIE_NAME,
|
|
||||||
value=consent.model_dump_json(),
|
|
||||||
httponly=True,
|
|
||||||
secure=settings.is_production,
|
|
||||||
samesite="lax",
|
|
||||||
max_age=365 * 24 * 60 * 60, # 1 year
|
|
||||||
path="/",
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
"Cookie consent updated: analytics=%s, marketing=%s, preferences=%s",
|
|
||||||
consent.categories.analytics,
|
|
||||||
consent.categories.marketing,
|
|
||||||
consent.categories.preferences,
|
|
||||||
)
|
|
||||||
|
|
||||||
return CookieConsentResponse(data=consent)
|
return CookieConsentResponse(data=consent)
|
||||||
|
|
||||||
|
@router.get('/config', response_model=PublicPrivacyConfigResponse, status_code=status.HTTP_200_OK)
|
||||||
@router.get(
|
async def get_public_privacy_config(db: Session=Depends(get_db)) -> PublicPrivacyConfigResponse:
|
||||||
"/config",
|
|
||||||
response_model=PublicPrivacyConfigResponse,
|
|
||||||
status_code=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
async def get_public_privacy_config(
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
) -> PublicPrivacyConfigResponse:
|
|
||||||
"""
|
|
||||||
Public privacy configuration for the frontend:
|
|
||||||
- Global policy flags
|
|
||||||
- Public integration IDs (e.g. GA measurement ID)
|
|
||||||
"""
|
|
||||||
config = privacy_admin_service.get_public_privacy_config(db)
|
config = privacy_admin_service.get_public_privacy_config(db)
|
||||||
return PublicPrivacyConfigResponse(data=config)
|
return PublicPrivacyConfigResponse(data=config)
|
||||||
|
|
||||||
|
|
||||||
@@ -3,346 +3,158 @@ from sqlalchemy.orm import Session
|
|||||||
from sqlalchemy import or_
|
from sqlalchemy import or_
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from ..config.database import get_db
|
from ..config.database import get_db
|
||||||
from ..middleware.auth import get_current_user, authorize_roles
|
from ..middleware.auth import get_current_user, authorize_roles
|
||||||
from ..models.user import User
|
from ..models.user import User
|
||||||
from ..models.promotion import Promotion, DiscountType
|
from ..models.promotion import Promotion, DiscountType
|
||||||
|
router = APIRouter(prefix='/promotions', tags=['promotions'])
|
||||||
|
|
||||||
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)):
|
||||||
|
|
||||||
@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:
|
try:
|
||||||
query = db.query(Promotion)
|
query = db.query(Promotion)
|
||||||
|
|
||||||
# Filter by search (code or name)
|
|
||||||
if search:
|
if search:
|
||||||
query = query.filter(
|
query = query.filter(or_(Promotion.code.like(f'%{search}%'), Promotion.name.like(f'%{search}%')))
|
||||||
or_(
|
|
||||||
Promotion.code.like(f"%{search}%"),
|
|
||||||
Promotion.name.like(f"%{search}%")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Filter by status (is_active)
|
|
||||||
if status_filter:
|
if status_filter:
|
||||||
is_active = status_filter == "active"
|
is_active = status_filter == 'active'
|
||||||
query = query.filter(Promotion.is_active == is_active)
|
query = query.filter(Promotion.is_active == is_active)
|
||||||
|
|
||||||
# Filter by discount type
|
|
||||||
if type:
|
if type:
|
||||||
try:
|
try:
|
||||||
query = query.filter(Promotion.discount_type == DiscountType(type))
|
query = query.filter(Promotion.discount_type == DiscountType(type))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
total = query.count()
|
total = query.count()
|
||||||
offset = (page - 1) * limit
|
offset = (page - 1) * limit
|
||||||
promotions = query.order_by(Promotion.created_at.desc()).offset(offset).limit(limit).all()
|
promotions = query.order_by(Promotion.created_at.desc()).offset(offset).limit(limit).all()
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for promo in promotions:
|
for promo in promotions:
|
||||||
promo_dict = {
|
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}
|
||||||
"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)
|
result.append(promo_dict)
|
||||||
|
return {'status': 'success', 'data': {'promotions': result, 'pagination': {'total': total, 'page': page, 'limit': limit, 'totalPages': (total + limit - 1) // limit}}}
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"data": {
|
|
||||||
"promotions": result,
|
|
||||||
"pagination": {
|
|
||||||
"total": total,
|
|
||||||
"page": page,
|
|
||||||
"limit": limit,
|
|
||||||
"totalPages": (total + limit - 1) // limit,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.get('/{code}')
|
||||||
@router.get("/{code}")
|
async def get_promotion_by_code(code: str, db: Session=Depends(get_db)):
|
||||||
async def get_promotion_by_code(code: str, db: Session = Depends(get_db)):
|
|
||||||
"""Get promotion by code"""
|
|
||||||
try:
|
try:
|
||||||
promotion = db.query(Promotion).filter(Promotion.code == code).first()
|
promotion = db.query(Promotion).filter(Promotion.code == code).first()
|
||||||
if not promotion:
|
if not promotion:
|
||||||
raise HTTPException(status_code=404, detail="Promotion not found")
|
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}
|
||||||
promo_dict = {
|
return {'status': 'success', 'data': {'promotion': 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:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.post('/validate')
|
||||||
@router.post("/validate")
|
async def validate_promotion(validation_data: dict, db: Session=Depends(get_db)):
|
||||||
async def validate_promotion(
|
|
||||||
validation_data: dict,
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Validate and apply promotion"""
|
|
||||||
try:
|
try:
|
||||||
code = validation_data.get("code")
|
code = validation_data.get('code')
|
||||||
# Accept both booking_value (from frontend) and booking_amount (for backward compatibility)
|
booking_amount = float(validation_data.get('booking_value') or validation_data.get('booking_amount', 0))
|
||||||
booking_amount = float(validation_data.get("booking_value") or validation_data.get("booking_amount", 0))
|
|
||||||
|
|
||||||
promotion = db.query(Promotion).filter(Promotion.code == code).first()
|
promotion = db.query(Promotion).filter(Promotion.code == code).first()
|
||||||
if not promotion:
|
if not promotion:
|
||||||
raise HTTPException(status_code=404, detail="Promotion code not found")
|
raise HTTPException(status_code=404, detail='Promotion code not found')
|
||||||
|
|
||||||
# Check if promotion is active
|
|
||||||
if not promotion.is_active:
|
if not promotion.is_active:
|
||||||
raise HTTPException(status_code=400, detail="Promotion is not active")
|
raise HTTPException(status_code=400, detail='Promotion is not active')
|
||||||
|
|
||||||
# Check date validity
|
|
||||||
now = datetime.utcnow()
|
now = datetime.utcnow()
|
||||||
if promotion.start_date and now < promotion.start_date:
|
if promotion.start_date and now < promotion.start_date:
|
||||||
raise HTTPException(status_code=400, detail="Promotion is not valid at this time")
|
raise HTTPException(status_code=400, detail='Promotion is not valid at this time')
|
||||||
if promotion.end_date and now > promotion.end_date:
|
if promotion.end_date and now > promotion.end_date:
|
||||||
raise HTTPException(status_code=400, detail="Promotion is not valid at this time")
|
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:
|
if promotion.usage_limit and promotion.used_count >= promotion.usage_limit:
|
||||||
raise HTTPException(status_code=400, detail="Promotion usage limit reached")
|
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):
|
if promotion.min_booking_amount and booking_amount < float(promotion.min_booking_amount):
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=400, detail=f'Minimum booking amount is {promotion.min_booking_amount}')
|
||||||
status_code=400,
|
|
||||||
detail=f"Minimum booking amount is {promotion.min_booking_amount}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Calculate discount
|
|
||||||
discount_amount = promotion.calculate_discount(booking_amount)
|
discount_amount = promotion.calculate_discount(booking_amount)
|
||||||
final_amount = booking_amount - discount_amount
|
final_amount = booking_amount - discount_amount
|
||||||
|
return {'success': True, 'status': 'success', 'data': {'promotion': {'id': promotion.id, 'code': promotion.code, 'name': promotion.name, 'description': promotion.description, 'discount_type': promotion.discount_type.value if hasattr(promotion.discount_type, 'value') else str(promotion.discount_type), 'discount_value': float(promotion.discount_value) if promotion.discount_value else 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, 'status': 'active' if promotion.is_active else 'inactive'}, 'discount': discount_amount, 'original_amount': booking_amount, 'discount_amount': discount_amount, 'final_amount': final_amount}, 'message': 'Promotion validated successfully'}
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"status": "success",
|
|
||||||
"data": {
|
|
||||||
"promotion": {
|
|
||||||
"id": promotion.id,
|
|
||||||
"code": promotion.code,
|
|
||||||
"name": promotion.name,
|
|
||||||
"description": promotion.description,
|
|
||||||
"discount_type": promotion.discount_type.value if hasattr(promotion.discount_type, 'value') else str(promotion.discount_type),
|
|
||||||
"discount_value": float(promotion.discount_value) if promotion.discount_value else 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,
|
|
||||||
"status": "active" if promotion.is_active else "inactive",
|
|
||||||
},
|
|
||||||
"discount": discount_amount,
|
|
||||||
"original_amount": booking_amount,
|
|
||||||
"discount_amount": discount_amount,
|
|
||||||
"final_amount": final_amount,
|
|
||||||
},
|
|
||||||
"message": "Promotion validated successfully"
|
|
||||||
}
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.post('/', dependencies=[Depends(authorize_roles('admin'))])
|
||||||
@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)):
|
||||||
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:
|
try:
|
||||||
code = promotion_data.get("code")
|
code = promotion_data.get('code')
|
||||||
|
|
||||||
# Check if code exists
|
|
||||||
existing = db.query(Promotion).filter(Promotion.code == code).first()
|
existing = db.query(Promotion).filter(Promotion.code == code).first()
|
||||||
if existing:
|
if existing:
|
||||||
raise HTTPException(status_code=400, detail="Promotion code already exists")
|
raise HTTPException(status_code=400, detail='Promotion code already exists')
|
||||||
|
discount_type = promotion_data.get('discount_type')
|
||||||
discount_type = promotion_data.get("discount_type")
|
discount_value = float(promotion_data.get('discount_value', 0))
|
||||||
discount_value = float(promotion_data.get("discount_value", 0))
|
if discount_type == 'percentage' and discount_value > 100:
|
||||||
|
raise HTTPException(status_code=400, detail='Percentage discount cannot exceed 100%')
|
||||||
# Validate discount value
|
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)
|
||||||
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.add(promotion)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(promotion)
|
db.refresh(promotion)
|
||||||
|
return {'status': 'success', 'message': 'Promotion created successfully', 'data': {'promotion': promotion}}
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"message": "Promotion created successfully",
|
|
||||||
"data": {"promotion": promotion}
|
|
||||||
}
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.put('/{id}', dependencies=[Depends(authorize_roles('admin'))])
|
||||||
@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)):
|
||||||
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:
|
try:
|
||||||
promotion = db.query(Promotion).filter(Promotion.id == id).first()
|
promotion = db.query(Promotion).filter(Promotion.id == id).first()
|
||||||
if not promotion:
|
if not promotion:
|
||||||
raise HTTPException(status_code=404, detail="Promotion not found")
|
raise HTTPException(status_code=404, detail='Promotion not found')
|
||||||
|
code = promotion_data.get('code')
|
||||||
# Check if new code exists (excluding current)
|
|
||||||
code = promotion_data.get("code")
|
|
||||||
if code and code != promotion.code:
|
if code and code != promotion.code:
|
||||||
existing = db.query(Promotion).filter(
|
existing = db.query(Promotion).filter(Promotion.code == code, Promotion.id != id).first()
|
||||||
Promotion.code == code,
|
|
||||||
Promotion.id != id
|
|
||||||
).first()
|
|
||||||
if existing:
|
if existing:
|
||||||
raise HTTPException(status_code=400, detail="Promotion code already exists")
|
raise HTTPException(status_code=400, detail='Promotion code already exists')
|
||||||
|
discount_type = promotion_data.get('discount_type', promotion.discount_type.value if isinstance(promotion.discount_type, DiscountType) else promotion.discount_type)
|
||||||
# Validate discount value
|
discount_value = promotion_data.get('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:
|
if discount_value is not None:
|
||||||
discount_value = float(discount_value)
|
discount_value = float(discount_value)
|
||||||
if discount_type == "percentage" and discount_value > 100:
|
if discount_type == 'percentage' and discount_value > 100:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=400, detail='Percentage discount cannot exceed 100%')
|
||||||
status_code=400,
|
if 'code' in promotion_data:
|
||||||
detail="Percentage discount cannot exceed 100%"
|
promotion.code = promotion_data['code']
|
||||||
)
|
if 'name' in promotion_data:
|
||||||
|
promotion.name = promotion_data['name']
|
||||||
# Update fields
|
if 'description' in promotion_data:
|
||||||
if "code" in promotion_data:
|
promotion.description = promotion_data['description']
|
||||||
promotion.code = promotion_data["code"]
|
if 'discount_type' in promotion_data:
|
||||||
if "name" in promotion_data:
|
promotion.discount_type = DiscountType(promotion_data['discount_type'])
|
||||||
promotion.name = promotion_data["name"]
|
if 'discount_value' in promotion_data:
|
||||||
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
|
promotion.discount_value = discount_value
|
||||||
if "min_booking_amount" in promotion_data:
|
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
|
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:
|
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
|
promotion.max_discount_amount = float(promotion_data['max_discount_amount']) if promotion_data['max_discount_amount'] else None
|
||||||
if "start_date" in promotion_data:
|
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
|
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:
|
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
|
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:
|
if 'usage_limit' in promotion_data:
|
||||||
promotion.usage_limit = promotion_data["usage_limit"]
|
promotion.usage_limit = promotion_data['usage_limit']
|
||||||
if "status" in promotion_data:
|
if 'status' in promotion_data:
|
||||||
promotion.is_active = promotion_data["status"] == "active"
|
promotion.is_active = promotion_data['status'] == 'active'
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(promotion)
|
db.refresh(promotion)
|
||||||
|
return {'status': 'success', 'message': 'Promotion updated successfully', 'data': {'promotion': promotion}}
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"message": "Promotion updated successfully",
|
|
||||||
"data": {"promotion": promotion}
|
|
||||||
}
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.delete('/{id}', dependencies=[Depends(authorize_roles('admin'))])
|
||||||
@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)):
|
||||||
async def delete_promotion(
|
|
||||||
id: int,
|
|
||||||
current_user: User = Depends(authorize_roles("admin")),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Delete promotion (Admin only)"""
|
|
||||||
try:
|
try:
|
||||||
promotion = db.query(Promotion).filter(Promotion.id == id).first()
|
promotion = db.query(Promotion).filter(Promotion.id == id).first()
|
||||||
if not promotion:
|
if not promotion:
|
||||||
raise HTTPException(status_code=404, detail="Promotion not found")
|
raise HTTPException(status_code=404, detail='Promotion not found')
|
||||||
|
|
||||||
db.delete(promotion)
|
db.delete(promotion)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
return {'status': 'success', 'message': 'Promotion deleted successfully'}
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"message": "Promotion deleted successfully"
|
|
||||||
}
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
@@ -3,7 +3,6 @@ from sqlalchemy.orm import Session
|
|||||||
from sqlalchemy import func, and_
|
from sqlalchemy import func, and_
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from ..config.database import get_db
|
from ..config.database import get_db
|
||||||
from ..middleware.auth import get_current_user, authorize_roles
|
from ..middleware.auth import get_current_user, authorize_roles
|
||||||
from ..models.user import User
|
from ..models.user import User
|
||||||
@@ -12,55 +11,34 @@ from ..models.payment import Payment, PaymentStatus
|
|||||||
from ..models.room import Room
|
from ..models.room import Room
|
||||||
from ..models.service_usage import ServiceUsage
|
from ..models.service_usage import ServiceUsage
|
||||||
from ..models.service import Service
|
from ..models.service import Service
|
||||||
|
router = APIRouter(prefix='/reports', tags=['reports'])
|
||||||
|
|
||||||
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)):
|
||||||
|
|
||||||
@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:
|
try:
|
||||||
# Parse dates if provided
|
|
||||||
start_date = None
|
start_date = None
|
||||||
end_date = None
|
end_date = None
|
||||||
if from_date:
|
if from_date:
|
||||||
try:
|
try:
|
||||||
start_date = datetime.strptime(from_date, "%Y-%m-%d")
|
start_date = datetime.strptime(from_date, '%Y-%m-%d')
|
||||||
except ValueError:
|
except ValueError:
|
||||||
start_date = datetime.fromisoformat(from_date.replace('Z', '+00:00'))
|
start_date = datetime.fromisoformat(from_date.replace('Z', '+00:00'))
|
||||||
if to_date:
|
if to_date:
|
||||||
try:
|
try:
|
||||||
end_date = datetime.strptime(to_date, "%Y-%m-%d")
|
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)
|
end_date = end_date.replace(hour=23, minute=59, second=59)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
end_date = datetime.fromisoformat(to_date.replace('Z', '+00:00'))
|
end_date = datetime.fromisoformat(to_date.replace('Z', '+00:00'))
|
||||||
|
|
||||||
# Base queries
|
|
||||||
booking_query = db.query(Booking)
|
booking_query = db.query(Booking)
|
||||||
payment_query = db.query(Payment).filter(Payment.payment_status == PaymentStatus.completed)
|
payment_query = db.query(Payment).filter(Payment.payment_status == PaymentStatus.completed)
|
||||||
|
|
||||||
# Apply date filters
|
|
||||||
if start_date:
|
if start_date:
|
||||||
booking_query = booking_query.filter(Booking.created_at >= start_date)
|
booking_query = booking_query.filter(Booking.created_at >= start_date)
|
||||||
payment_query = payment_query.filter(Payment.payment_date >= start_date)
|
payment_query = payment_query.filter(Payment.payment_date >= start_date)
|
||||||
if end_date:
|
if end_date:
|
||||||
booking_query = booking_query.filter(Booking.created_at <= end_date)
|
booking_query = booking_query.filter(Booking.created_at <= end_date)
|
||||||
payment_query = payment_query.filter(Payment.payment_date <= end_date)
|
payment_query = payment_query.filter(Payment.payment_date <= end_date)
|
||||||
|
|
||||||
# Total bookings
|
|
||||||
total_bookings = booking_query.count()
|
total_bookings = booking_query.count()
|
||||||
|
|
||||||
# Total revenue
|
|
||||||
total_revenue = payment_query.with_entities(func.sum(Payment.amount)).scalar() or 0.0
|
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
|
total_customers = db.query(func.count(func.distinct(Booking.user_id))).scalar() or 0
|
||||||
if start_date or end_date:
|
if start_date or end_date:
|
||||||
customer_query = db.query(func.count(func.distinct(Booking.user_id)))
|
customer_query = db.query(func.count(func.distinct(Booking.user_id)))
|
||||||
@@ -69,415 +47,126 @@ async def get_reports(
|
|||||||
if end_date:
|
if end_date:
|
||||||
customer_query = customer_query.filter(Booking.created_at <= end_date)
|
customer_query = customer_query.filter(Booking.created_at <= end_date)
|
||||||
total_customers = customer_query.scalar() or 0
|
total_customers = customer_query.scalar() or 0
|
||||||
|
available_rooms = db.query(Room).filter(Room.status == 'available').count()
|
||||||
# Available rooms
|
occupied_rooms = db.query(func.count(func.distinct(Booking.room_id))).filter(Booking.status.in_([BookingStatus.confirmed, BookingStatus.checked_in])).scalar() or 0
|
||||||
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 = []
|
revenue_by_date = []
|
||||||
if start_date and end_date:
|
if start_date and end_date:
|
||||||
daily_revenue_query = db.query(
|
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)
|
||||||
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:
|
if start_date:
|
||||||
daily_revenue_query = daily_revenue_query.filter(Payment.payment_date >= start_date)
|
daily_revenue_query = daily_revenue_query.filter(Payment.payment_date >= start_date)
|
||||||
if end_date:
|
if end_date:
|
||||||
daily_revenue_query = daily_revenue_query.filter(Payment.payment_date <= 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_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()
|
daily_data = daily_revenue_query.all()
|
||||||
revenue_by_date = [
|
revenue_by_date = [{'date': str(date), 'revenue': float(revenue or 0), 'bookings': int(bookings or 0)} for date, revenue, bookings in daily_data]
|
||||||
{
|
|
||||||
"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 = {}
|
bookings_by_status = {}
|
||||||
for status in BookingStatus:
|
for status in BookingStatus:
|
||||||
count = booking_query.filter(Booking.status == status).count()
|
count = booking_query.filter(Booking.status == status).count()
|
||||||
status_name = status.value if hasattr(status, 'value') else str(status)
|
status_name = status.value if hasattr(status, 'value') else str(status)
|
||||||
bookings_by_status[status_name] = count
|
bookings_by_status[status_name] = count
|
||||||
|
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)
|
||||||
# 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:
|
if start_date:
|
||||||
top_rooms_query = top_rooms_query.filter(Booking.created_at >= start_date)
|
top_rooms_query = top_rooms_query.filter(Booking.created_at >= start_date)
|
||||||
if end_date:
|
if end_date:
|
||||||
top_rooms_query = top_rooms_query.filter(Booking.created_at <= 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_data = top_rooms_query.group_by(Room.id, Room.room_number).order_by(
|
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]
|
||||||
func.sum(Payment.amount).desc()
|
service_usage_query = db.query(Service.id, Service.name, func.count(ServiceUsage.id).label('usage_count'), func.sum(ServiceUsage.total_price).label('total_revenue')).join(ServiceUsage, Service.id == ServiceUsage.service_id)
|
||||||
).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
|
|
||||||
]
|
|
||||||
|
|
||||||
# Service usage statistics
|
|
||||||
service_usage_query = db.query(
|
|
||||||
Service.id,
|
|
||||||
Service.name,
|
|
||||||
func.count(ServiceUsage.id).label('usage_count'),
|
|
||||||
func.sum(ServiceUsage.total_price).label('total_revenue')
|
|
||||||
).join(ServiceUsage, Service.id == ServiceUsage.service_id)
|
|
||||||
|
|
||||||
if start_date:
|
if start_date:
|
||||||
service_usage_query = service_usage_query.filter(ServiceUsage.usage_date >= start_date)
|
service_usage_query = service_usage_query.filter(ServiceUsage.usage_date >= start_date)
|
||||||
if end_date:
|
if end_date:
|
||||||
service_usage_query = service_usage_query.filter(ServiceUsage.usage_date <= end_date)
|
service_usage_query = service_usage_query.filter(ServiceUsage.usage_date <= end_date)
|
||||||
|
service_usage_data = service_usage_query.group_by(Service.id, Service.name).order_by(func.sum(ServiceUsage.total_price).desc()).limit(10).all()
|
||||||
service_usage_data = service_usage_query.group_by(Service.id, Service.name).order_by(
|
service_usage = [{'service_id': service_id, 'service_name': service_name, 'usage_count': int(usage_count or 0), 'total_revenue': float(total_revenue or 0)} for service_id, service_name, usage_count, total_revenue in service_usage_data]
|
||||||
func.sum(ServiceUsage.total_price).desc()
|
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, 'service_usage': service_usage if service_usage else None}}
|
||||||
).limit(10).all()
|
|
||||||
|
|
||||||
service_usage = [
|
|
||||||
{
|
|
||||||
"service_id": service_id,
|
|
||||||
"service_name": service_name,
|
|
||||||
"usage_count": int(usage_count or 0),
|
|
||||||
"total_revenue": float(total_revenue or 0)
|
|
||||||
}
|
|
||||||
for service_id, service_name, usage_count, total_revenue in service_usage_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,
|
|
||||||
"service_usage": service_usage if service_usage else None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.get('/dashboard')
|
||||||
@router.get("/dashboard")
|
async def get_dashboard_stats(current_user: User=Depends(authorize_roles('admin', 'staff')), db: Session=Depends(get_db)):
|
||||||
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:
|
try:
|
||||||
# Total bookings
|
|
||||||
total_bookings = db.query(Booking).count()
|
total_bookings = db.query(Booking).count()
|
||||||
|
active_bookings = db.query(Booking).filter(Booking.status.in_([BookingStatus.pending, BookingStatus.confirmed, BookingStatus.checked_in])).count()
|
||||||
# Active bookings
|
total_revenue = db.query(func.sum(Payment.amount)).filter(Payment.payment_status == PaymentStatus.completed).scalar() or 0.0
|
||||||
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_start = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
today_revenue = db.query(func.sum(Payment.amount)).filter(
|
today_revenue = db.query(func.sum(Payment.amount)).filter(and_(Payment.payment_status == PaymentStatus.completed, Payment.payment_date >= today_start)).scalar() or 0.0
|
||||||
and_(
|
|
||||||
Payment.payment_status == PaymentStatus.completed,
|
|
||||||
Payment.payment_date >= today_start
|
|
||||||
)
|
|
||||||
).scalar() or 0.0
|
|
||||||
|
|
||||||
# Total rooms
|
|
||||||
total_rooms = db.query(Room).count()
|
total_rooms = db.query(Room).count()
|
||||||
|
available_rooms = db.query(Room).filter(Room.status == 'available').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)
|
week_ago = datetime.utcnow() - timedelta(days=7)
|
||||||
recent_bookings = db.query(Booking).filter(
|
recent_bookings = db.query(Booking).filter(Booking.created_at >= week_ago).count()
|
||||||
Booking.created_at >= week_ago
|
pending_payments = db.query(Payment).filter(Payment.payment_status == PaymentStatus.pending).count()
|
||||||
).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}}
|
||||||
|
|
||||||
# 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:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.get('/customer/dashboard')
|
||||||
@router.get("/customer/dashboard")
|
async def get_customer_dashboard_stats(current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||||
async def get_customer_dashboard_stats(
|
|
||||||
current_user: User = Depends(get_current_user),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Get customer dashboard statistics"""
|
|
||||||
try:
|
try:
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
total_bookings = db.query(Booking).filter(Booking.user_id == current_user.id).count()
|
||||||
# Total bookings count for user
|
user_bookings = db.query(Booking.id).filter(Booking.user_id == current_user.id).subquery()
|
||||||
total_bookings = db.query(Booking).filter(
|
total_spending = db.query(func.sum(Payment.amount)).filter(and_(Payment.booking_id.in_(db.query(user_bookings.c.id)), Payment.payment_status == PaymentStatus.completed)).scalar() or 0.0
|
||||||
Booking.user_id == current_user.id
|
|
||||||
).count()
|
|
||||||
|
|
||||||
# Total spending (sum of completed payments from user's bookings)
|
|
||||||
user_bookings = db.query(Booking.id).filter(
|
|
||||||
Booking.user_id == current_user.id
|
|
||||||
).subquery()
|
|
||||||
|
|
||||||
total_spending = db.query(func.sum(Payment.amount)).filter(
|
|
||||||
and_(
|
|
||||||
Payment.booking_id.in_(db.query(user_bookings.c.id)),
|
|
||||||
Payment.payment_status == PaymentStatus.completed
|
|
||||||
)
|
|
||||||
).scalar() or 0.0
|
|
||||||
|
|
||||||
# Currently staying (checked_in bookings)
|
|
||||||
now = datetime.utcnow()
|
now = datetime.utcnow()
|
||||||
currently_staying = db.query(Booking).filter(
|
currently_staying = db.query(Booking).filter(and_(Booking.user_id == current_user.id, Booking.status == BookingStatus.checked_in, Booking.check_in_date <= now, Booking.check_out_date >= now)).count()
|
||||||
and_(
|
upcoming_bookings_query = db.query(Booking).filter(and_(Booking.user_id == current_user.id, Booking.status.in_([BookingStatus.confirmed, BookingStatus.pending]), Booking.check_in_date > now)).order_by(Booking.check_in_date.asc()).limit(5).all()
|
||||||
Booking.user_id == current_user.id,
|
|
||||||
Booking.status == BookingStatus.checked_in,
|
|
||||||
Booking.check_in_date <= now,
|
|
||||||
Booking.check_out_date >= now
|
|
||||||
)
|
|
||||||
).count()
|
|
||||||
|
|
||||||
# Upcoming bookings (confirmed/pending with check_in_date in future)
|
|
||||||
upcoming_bookings_query = db.query(Booking).filter(
|
|
||||||
and_(
|
|
||||||
Booking.user_id == current_user.id,
|
|
||||||
Booking.status.in_([BookingStatus.confirmed, BookingStatus.pending]),
|
|
||||||
Booking.check_in_date > now
|
|
||||||
)
|
|
||||||
).order_by(Booking.check_in_date.asc()).limit(5).all()
|
|
||||||
|
|
||||||
upcoming_bookings = []
|
upcoming_bookings = []
|
||||||
for booking in upcoming_bookings_query:
|
for booking in upcoming_bookings_query:
|
||||||
booking_dict = {
|
booking_dict = {'id': booking.id, 'booking_number': booking.booking_number, '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, 'total_price': float(booking.total_price) if booking.total_price else 0.0}
|
||||||
"id": booking.id,
|
|
||||||
"booking_number": booking.booking_number,
|
|
||||||
"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,
|
|
||||||
"total_price": float(booking.total_price) if booking.total_price else 0.0,
|
|
||||||
}
|
|
||||||
|
|
||||||
if booking.room:
|
if booking.room:
|
||||||
booking_dict["room"] = {
|
booking_dict['room'] = {'id': booking.room.id, 'room_number': booking.room.room_number, 'room_type': {'name': booking.room.room_type.name if booking.room.room_type else None}}
|
||||||
"id": booking.room.id,
|
|
||||||
"room_number": booking.room.room_number,
|
|
||||||
"room_type": {
|
|
||||||
"name": booking.room.room_type.name if booking.room.room_type else None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
upcoming_bookings.append(booking_dict)
|
upcoming_bookings.append(booking_dict)
|
||||||
|
recent_bookings_query = db.query(Booking).filter(Booking.user_id == current_user.id).order_by(Booking.created_at.desc()).limit(5).all()
|
||||||
# Recent activity (last 5 bookings ordered by created_at)
|
|
||||||
recent_bookings_query = db.query(Booking).filter(
|
|
||||||
Booking.user_id == current_user.id
|
|
||||||
).order_by(Booking.created_at.desc()).limit(5).all()
|
|
||||||
|
|
||||||
recent_activity = []
|
recent_activity = []
|
||||||
for booking in recent_bookings_query:
|
for booking in recent_bookings_query:
|
||||||
activity_type = None
|
activity_type = None
|
||||||
if booking.status == BookingStatus.checked_out:
|
if booking.status == BookingStatus.checked_out:
|
||||||
activity_type = "Check-out"
|
activity_type = 'Check-out'
|
||||||
elif booking.status == BookingStatus.checked_in:
|
elif booking.status == BookingStatus.checked_in:
|
||||||
activity_type = "Check-in"
|
activity_type = 'Check-in'
|
||||||
elif booking.status == BookingStatus.confirmed:
|
elif booking.status == BookingStatus.confirmed:
|
||||||
activity_type = "Booking Confirmed"
|
activity_type = 'Booking Confirmed'
|
||||||
elif booking.status == BookingStatus.pending:
|
elif booking.status == BookingStatus.pending:
|
||||||
activity_type = "Booking"
|
activity_type = 'Booking'
|
||||||
else:
|
else:
|
||||||
activity_type = "Booking"
|
activity_type = 'Booking'
|
||||||
|
activity_dict = {'action': activity_type, 'booking_id': booking.id, 'booking_number': booking.booking_number, 'created_at': booking.created_at.isoformat() if booking.created_at else None}
|
||||||
activity_dict = {
|
|
||||||
"action": activity_type,
|
|
||||||
"booking_id": booking.id,
|
|
||||||
"booking_number": booking.booking_number,
|
|
||||||
"created_at": booking.created_at.isoformat() if booking.created_at else None,
|
|
||||||
}
|
|
||||||
|
|
||||||
if booking.room:
|
if booking.room:
|
||||||
activity_dict["room"] = {
|
activity_dict['room'] = {'room_number': booking.room.room_number}
|
||||||
"room_number": booking.room.room_number,
|
|
||||||
}
|
|
||||||
|
|
||||||
recent_activity.append(activity_dict)
|
recent_activity.append(activity_dict)
|
||||||
|
|
||||||
# Calculate percentage change (placeholder - can be enhanced)
|
|
||||||
# For now, compare last month vs this month
|
|
||||||
last_month_start = (now - timedelta(days=30)).replace(day=1, hour=0, minute=0, second=0)
|
last_month_start = (now - timedelta(days=30)).replace(day=1, hour=0, minute=0, second=0)
|
||||||
last_month_end = now.replace(day=1, hour=0, minute=0, second=0) - timedelta(seconds=1)
|
last_month_end = now.replace(day=1, hour=0, minute=0, second=0) - timedelta(seconds=1)
|
||||||
|
last_month_bookings = db.query(Booking).filter(and_(Booking.user_id == current_user.id, Booking.created_at >= last_month_start, Booking.created_at <= last_month_end)).count()
|
||||||
last_month_bookings = db.query(Booking).filter(
|
this_month_bookings = db.query(Booking).filter(and_(Booking.user_id == current_user.id, Booking.created_at >= now.replace(day=1, hour=0, minute=0, second=0), Booking.created_at <= now)).count()
|
||||||
and_(
|
|
||||||
Booking.user_id == current_user.id,
|
|
||||||
Booking.created_at >= last_month_start,
|
|
||||||
Booking.created_at <= last_month_end
|
|
||||||
)
|
|
||||||
).count()
|
|
||||||
|
|
||||||
this_month_bookings = db.query(Booking).filter(
|
|
||||||
and_(
|
|
||||||
Booking.user_id == current_user.id,
|
|
||||||
Booking.created_at >= now.replace(day=1, hour=0, minute=0, second=0),
|
|
||||||
Booking.created_at <= now
|
|
||||||
)
|
|
||||||
).count()
|
|
||||||
|
|
||||||
booking_change_percentage = 0
|
booking_change_percentage = 0
|
||||||
if last_month_bookings > 0:
|
if last_month_bookings > 0:
|
||||||
booking_change_percentage = ((this_month_bookings - last_month_bookings) / last_month_bookings) * 100
|
booking_change_percentage = (this_month_bookings - last_month_bookings) / last_month_bookings * 100
|
||||||
|
last_month_spending = db.query(func.sum(Payment.amount)).filter(and_(Payment.booking_id.in_(db.query(user_bookings.c.id)), Payment.payment_status == PaymentStatus.completed, Payment.payment_date >= last_month_start, Payment.payment_date <= last_month_end)).scalar() or 0.0
|
||||||
last_month_spending = db.query(func.sum(Payment.amount)).filter(
|
this_month_spending = db.query(func.sum(Payment.amount)).filter(and_(Payment.booking_id.in_(db.query(user_bookings.c.id)), Payment.payment_status == PaymentStatus.completed, Payment.payment_date >= now.replace(day=1, hour=0, minute=0, second=0), Payment.payment_date <= now)).scalar() or 0.0
|
||||||
and_(
|
|
||||||
Payment.booking_id.in_(db.query(user_bookings.c.id)),
|
|
||||||
Payment.payment_status == PaymentStatus.completed,
|
|
||||||
Payment.payment_date >= last_month_start,
|
|
||||||
Payment.payment_date <= last_month_end
|
|
||||||
)
|
|
||||||
).scalar() or 0.0
|
|
||||||
|
|
||||||
this_month_spending = db.query(func.sum(Payment.amount)).filter(
|
|
||||||
and_(
|
|
||||||
Payment.booking_id.in_(db.query(user_bookings.c.id)),
|
|
||||||
Payment.payment_status == PaymentStatus.completed,
|
|
||||||
Payment.payment_date >= now.replace(day=1, hour=0, minute=0, second=0),
|
|
||||||
Payment.payment_date <= now
|
|
||||||
)
|
|
||||||
).scalar() or 0.0
|
|
||||||
|
|
||||||
spending_change_percentage = 0
|
spending_change_percentage = 0
|
||||||
if last_month_spending > 0:
|
if last_month_spending > 0:
|
||||||
spending_change_percentage = ((this_month_spending - last_month_spending) / last_month_spending) * 100
|
spending_change_percentage = (this_month_spending - last_month_spending) / last_month_spending * 100
|
||||||
|
return {'status': 'success', 'success': True, 'data': {'total_bookings': total_bookings, 'total_spending': float(total_spending), 'currently_staying': currently_staying, 'upcoming_bookings': upcoming_bookings, 'recent_activity': recent_activity, 'booking_change_percentage': round(booking_change_percentage, 1), 'spending_change_percentage': round(spending_change_percentage, 1)}}
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"success": True,
|
|
||||||
"data": {
|
|
||||||
"total_bookings": total_bookings,
|
|
||||||
"total_spending": float(total_spending),
|
|
||||||
"currently_staying": currently_staying,
|
|
||||||
"upcoming_bookings": upcoming_bookings,
|
|
||||||
"recent_activity": recent_activity,
|
|
||||||
"booking_change_percentage": round(booking_change_percentage, 1),
|
|
||||||
"spending_change_percentage": round(spending_change_percentage, 1),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.get('/revenue')
|
||||||
@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)):
|
||||||
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:
|
try:
|
||||||
query = db.query(Payment).filter(
|
query = db.query(Payment).filter(Payment.payment_status == PaymentStatus.completed)
|
||||||
Payment.payment_status == PaymentStatus.completed
|
|
||||||
)
|
|
||||||
|
|
||||||
if start_date:
|
if start_date:
|
||||||
start = datetime.fromisoformat(start_date.replace('Z', '+00:00'))
|
start = datetime.fromisoformat(start_date.replace('Z', '+00:00'))
|
||||||
query = query.filter(Payment.payment_date >= start)
|
query = query.filter(Payment.payment_date >= start)
|
||||||
|
|
||||||
if end_date:
|
if end_date:
|
||||||
end = datetime.fromisoformat(end_date.replace('Z', '+00:00'))
|
end = datetime.fromisoformat(end_date.replace('Z', '+00:00'))
|
||||||
query = query.filter(Payment.payment_date <= end)
|
query = query.filter(Payment.payment_date <= end)
|
||||||
|
total_revenue = db.query(func.sum(Payment.amount)).filter(Payment.payment_status == PaymentStatus.completed).scalar() or 0.0
|
||||||
# Total revenue
|
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()
|
||||||
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 = {}
|
method_breakdown = {}
|
||||||
for method, total in revenue_by_method:
|
for method, total in revenue_by_method:
|
||||||
method_name = method.value if hasattr(method, 'value') else str(method)
|
method_name = method.value if hasattr(method, 'value') else str(method)
|
||||||
method_breakdown[method_name] = float(total or 0)
|
method_breakdown[method_name] = float(total or 0)
|
||||||
|
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()
|
||||||
# Revenue by date (daily breakdown)
|
daily_breakdown = [{'date': date.isoformat() if isinstance(date, datetime) else str(date), 'revenue': float(total or 0)} for date, total in daily_revenue]
|
||||||
daily_revenue = db.query(
|
return {'status': 'success', 'data': {'total_revenue': float(total_revenue), 'revenue_by_method': method_breakdown, 'daily_breakdown': daily_breakdown}}
|
||||||
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:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
@@ -1,251 +1,117 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from ..config.database import get_db
|
from ..config.database import get_db
|
||||||
from ..middleware.auth import get_current_user, authorize_roles
|
from ..middleware.auth import get_current_user, authorize_roles
|
||||||
from ..models.user import User
|
from ..models.user import User
|
||||||
from ..models.review import Review, ReviewStatus
|
from ..models.review import Review, ReviewStatus
|
||||||
from ..models.room import Room
|
from ..models.room import Room
|
||||||
|
router = APIRouter(prefix='/reviews', tags=['reviews'])
|
||||||
|
|
||||||
router = APIRouter(prefix="/reviews", tags=["reviews"])
|
@router.get('/room/{room_id}')
|
||||||
|
async def get_room_reviews(room_id: int, db: Session=Depends(get_db)):
|
||||||
|
|
||||||
@router.get("/room/{room_id}")
|
|
||||||
async def get_room_reviews(room_id: int, db: Session = Depends(get_db)):
|
|
||||||
"""Get reviews for a room"""
|
|
||||||
try:
|
try:
|
||||||
reviews = db.query(Review).filter(
|
reviews = db.query(Review).filter(Review.room_id == room_id, Review.status == ReviewStatus.approved).order_by(Review.created_at.desc()).all()
|
||||||
Review.room_id == room_id,
|
|
||||||
Review.status == ReviewStatus.approved
|
|
||||||
).order_by(Review.created_at.desc()).all()
|
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for review in reviews:
|
for review in reviews:
|
||||||
review_dict = {
|
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}
|
||||||
"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:
|
if review.user:
|
||||||
review_dict["user"] = {
|
review_dict['user'] = {'id': review.user.id, 'full_name': review.user.full_name, 'email': review.user.email}
|
||||||
"id": review.user.id,
|
|
||||||
"full_name": review.user.full_name,
|
|
||||||
"email": review.user.email,
|
|
||||||
}
|
|
||||||
|
|
||||||
result.append(review_dict)
|
result.append(review_dict)
|
||||||
|
return {'status': 'success', 'data': {'reviews': result}}
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"data": {"reviews": result}
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.get('/', dependencies=[Depends(authorize_roles('admin'))])
|
||||||
@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)):
|
||||||
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:
|
try:
|
||||||
query = db.query(Review)
|
query = db.query(Review)
|
||||||
|
|
||||||
if status_filter:
|
if status_filter:
|
||||||
try:
|
try:
|
||||||
query = query.filter(Review.status == ReviewStatus(status_filter))
|
query = query.filter(Review.status == ReviewStatus(status_filter))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
total = query.count()
|
total = query.count()
|
||||||
offset = (page - 1) * limit
|
offset = (page - 1) * limit
|
||||||
reviews = query.order_by(Review.created_at.desc()).offset(offset).limit(limit).all()
|
reviews = query.order_by(Review.created_at.desc()).offset(offset).limit(limit).all()
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for review in reviews:
|
for review in reviews:
|
||||||
review_dict = {
|
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}
|
||||||
"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:
|
if review.user:
|
||||||
review_dict["user"] = {
|
review_dict['user'] = {'id': review.user.id, 'full_name': review.user.full_name, 'email': review.user.email, 'phone': review.user.phone}
|
||||||
"id": review.user.id,
|
|
||||||
"full_name": review.user.full_name,
|
|
||||||
"email": review.user.email,
|
|
||||||
"phone": review.user.phone,
|
|
||||||
}
|
|
||||||
|
|
||||||
if review.room:
|
if review.room:
|
||||||
review_dict["room"] = {
|
review_dict['room'] = {'id': review.room.id, 'room_number': review.room.room_number}
|
||||||
"id": review.room.id,
|
|
||||||
"room_number": review.room.room_number,
|
|
||||||
}
|
|
||||||
|
|
||||||
result.append(review_dict)
|
result.append(review_dict)
|
||||||
|
return {'status': 'success', 'data': {'reviews': result, 'pagination': {'total': total, 'page': page, 'limit': limit, 'totalPages': (total + limit - 1) // limit}}}
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"data": {
|
|
||||||
"reviews": result,
|
|
||||||
"pagination": {
|
|
||||||
"total": total,
|
|
||||||
"page": page,
|
|
||||||
"limit": limit,
|
|
||||||
"totalPages": (total + limit - 1) // limit,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.post('/')
|
||||||
@router.post("/")
|
async def create_review(review_data: dict, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||||
async def create_review(
|
|
||||||
review_data: dict,
|
|
||||||
current_user: User = Depends(get_current_user),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Create new review"""
|
|
||||||
try:
|
try:
|
||||||
room_id = review_data.get("room_id")
|
room_id = review_data.get('room_id')
|
||||||
rating = review_data.get("rating")
|
rating = review_data.get('rating')
|
||||||
comment = review_data.get("comment")
|
comment = review_data.get('comment')
|
||||||
|
|
||||||
# Check if room exists
|
|
||||||
room = db.query(Room).filter(Room.id == room_id).first()
|
room = db.query(Room).filter(Room.id == room_id).first()
|
||||||
if not room:
|
if not room:
|
||||||
raise HTTPException(status_code=404, detail="Room not found")
|
raise HTTPException(status_code=404, detail='Room not found')
|
||||||
|
existing = db.query(Review).filter(Review.user_id == current_user.id, Review.room_id == room_id).first()
|
||||||
# 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:
|
if existing:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=400, detail='You have already reviewed this room')
|
||||||
status_code=400,
|
review = Review(user_id=current_user.id, room_id=room_id, rating=rating, comment=comment, status=ReviewStatus.pending)
|
||||||
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.add(review)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(review)
|
db.refresh(review)
|
||||||
|
return {'status': 'success', 'message': 'Review submitted successfully and is pending approval', 'data': {'review': review}}
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"message": "Review submitted successfully and is pending approval",
|
|
||||||
"data": {"review": review}
|
|
||||||
}
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.put('/{id}/approve', dependencies=[Depends(authorize_roles('admin'))])
|
||||||
@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)):
|
||||||
async def approve_review(
|
|
||||||
id: int,
|
|
||||||
current_user: User = Depends(authorize_roles("admin")),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Approve review (Admin only)"""
|
|
||||||
try:
|
try:
|
||||||
review = db.query(Review).filter(Review.id == id).first()
|
review = db.query(Review).filter(Review.id == id).first()
|
||||||
if not review:
|
if not review:
|
||||||
raise HTTPException(status_code=404, detail="Review not found")
|
raise HTTPException(status_code=404, detail='Review not found')
|
||||||
|
|
||||||
review.status = ReviewStatus.approved
|
review.status = ReviewStatus.approved
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(review)
|
db.refresh(review)
|
||||||
|
return {'status': 'success', 'message': 'Review approved successfully', 'data': {'review': review}}
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"message": "Review approved successfully",
|
|
||||||
"data": {"review": review}
|
|
||||||
}
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.put('/{id}/reject', dependencies=[Depends(authorize_roles('admin'))])
|
||||||
@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)):
|
||||||
async def reject_review(
|
|
||||||
id: int,
|
|
||||||
current_user: User = Depends(authorize_roles("admin")),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Reject review (Admin only)"""
|
|
||||||
try:
|
try:
|
||||||
review = db.query(Review).filter(Review.id == id).first()
|
review = db.query(Review).filter(Review.id == id).first()
|
||||||
if not review:
|
if not review:
|
||||||
raise HTTPException(status_code=404, detail="Review not found")
|
raise HTTPException(status_code=404, detail='Review not found')
|
||||||
|
|
||||||
review.status = ReviewStatus.rejected
|
review.status = ReviewStatus.rejected
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(review)
|
db.refresh(review)
|
||||||
|
return {'status': 'success', 'message': 'Review rejected successfully', 'data': {'review': review}}
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"message": "Review rejected successfully",
|
|
||||||
"data": {"review": review}
|
|
||||||
}
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.delete('/{id}', dependencies=[Depends(authorize_roles('admin'))])
|
||||||
@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)):
|
||||||
async def delete_review(
|
|
||||||
id: int,
|
|
||||||
current_user: User = Depends(authorize_roles("admin")),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Delete review (Admin only)"""
|
|
||||||
try:
|
try:
|
||||||
review = db.query(Review).filter(Review.id == id).first()
|
review = db.query(Review).filter(Review.id == id).first()
|
||||||
if not review:
|
if not review:
|
||||||
raise HTTPException(status_code=404, detail="Review not found")
|
raise HTTPException(status_code=404, detail='Review not found')
|
||||||
|
|
||||||
db.delete(review)
|
db.delete(review)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
return {'status': 'success', 'message': 'Review deleted successfully'}
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"message": "Review deleted successfully"
|
|
||||||
}
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -21,22 +21,18 @@ from ..config.settings import settings
|
|||||||
|
|
||||||
router = APIRouter(prefix="/service-bookings", tags=["service-bookings"])
|
router = APIRouter(prefix="/service-bookings", tags=["service-bookings"])
|
||||||
|
|
||||||
|
|
||||||
def generate_service_booking_number() -> str:
|
def generate_service_booking_number() -> str:
|
||||||
"""Generate unique service booking number"""
|
|
||||||
prefix = "SB"
|
prefix = "SB"
|
||||||
timestamp = datetime.utcnow().strftime("%Y%m%d")
|
timestamp = datetime.utcnow().strftime("%Y%m%d")
|
||||||
random_suffix = random.randint(1000, 9999)
|
random_suffix = random.randint(1000, 9999)
|
||||||
return f"{prefix}{timestamp}{random_suffix}"
|
return f"{prefix}{timestamp}{random_suffix}"
|
||||||
|
|
||||||
|
|
||||||
@router.post("/")
|
@router.post("/")
|
||||||
async def create_service_booking(
|
async def create_service_booking(
|
||||||
booking_data: dict,
|
booking_data: dict,
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""Create a new service booking"""
|
|
||||||
try:
|
try:
|
||||||
services = booking_data.get("services", [])
|
services = booking_data.get("services", [])
|
||||||
total_amount = float(booking_data.get("total_amount", 0))
|
total_amount = float(booking_data.get("total_amount", 0))
|
||||||
@@ -48,7 +44,7 @@ async def create_service_booking(
|
|||||||
if total_amount <= 0:
|
if total_amount <= 0:
|
||||||
raise HTTPException(status_code=400, detail="Total amount must be greater than 0")
|
raise HTTPException(status_code=400, detail="Total amount must be greater than 0")
|
||||||
|
|
||||||
# Validate services and calculate total
|
|
||||||
calculated_total = 0
|
calculated_total = 0
|
||||||
service_items_data = []
|
service_items_data = []
|
||||||
|
|
||||||
@@ -59,7 +55,7 @@ async def create_service_booking(
|
|||||||
if not service_id:
|
if not service_id:
|
||||||
raise HTTPException(status_code=400, detail="Service ID is required for each item")
|
raise HTTPException(status_code=400, detail="Service ID is required for each item")
|
||||||
|
|
||||||
# Check if service exists and is active
|
|
||||||
service = db.query(Service).filter(Service.id == service_id).first()
|
service = db.query(Service).filter(Service.id == service_id).first()
|
||||||
if not service:
|
if not service:
|
||||||
raise HTTPException(status_code=404, detail=f"Service with ID {service_id} not found")
|
raise HTTPException(status_code=404, detail=f"Service with ID {service_id} not found")
|
||||||
@@ -78,17 +74,17 @@ async def create_service_booking(
|
|||||||
"total_price": item_total
|
"total_price": item_total
|
||||||
})
|
})
|
||||||
|
|
||||||
# Verify calculated total matches provided total (with small tolerance for floating point)
|
|
||||||
if abs(calculated_total - total_amount) > 0.01:
|
if abs(calculated_total - total_amount) > 0.01:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail=f"Total amount mismatch. Calculated: {calculated_total}, Provided: {total_amount}"
|
detail=f"Total amount mismatch. Calculated: {calculated_total}, Provided: {total_amount}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Generate booking number
|
|
||||||
booking_number = generate_service_booking_number()
|
booking_number = generate_service_booking_number()
|
||||||
|
|
||||||
# Create service booking
|
|
||||||
service_booking = ServiceBooking(
|
service_booking = ServiceBooking(
|
||||||
booking_number=booking_number,
|
booking_number=booking_number,
|
||||||
user_id=current_user.id,
|
user_id=current_user.id,
|
||||||
@@ -98,9 +94,9 @@ async def create_service_booking(
|
|||||||
)
|
)
|
||||||
|
|
||||||
db.add(service_booking)
|
db.add(service_booking)
|
||||||
db.flush() # Flush to get the ID
|
db.flush()
|
||||||
|
|
||||||
# Create service booking items
|
|
||||||
for item_data in service_items_data:
|
for item_data in service_items_data:
|
||||||
booking_item = ServiceBookingItem(
|
booking_item = ServiceBookingItem(
|
||||||
service_booking_id=service_booking.id,
|
service_booking_id=service_booking.id,
|
||||||
@@ -114,12 +110,12 @@ async def create_service_booking(
|
|||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(service_booking)
|
db.refresh(service_booking)
|
||||||
|
|
||||||
# Load relationships
|
|
||||||
service_booking = db.query(ServiceBooking).options(
|
service_booking = db.query(ServiceBooking).options(
|
||||||
joinedload(ServiceBooking.service_items).joinedload(ServiceBookingItem.service)
|
joinedload(ServiceBooking.service_items).joinedload(ServiceBookingItem.service)
|
||||||
).filter(ServiceBooking.id == service_booking.id).first()
|
).filter(ServiceBooking.id == service_booking.id).first()
|
||||||
|
|
||||||
# Format response
|
|
||||||
booking_dict = {
|
booking_dict = {
|
||||||
"id": service_booking.id,
|
"id": service_booking.id,
|
||||||
"booking_number": service_booking.booking_number,
|
"booking_number": service_booking.booking_number,
|
||||||
@@ -157,13 +153,11 @@ async def create_service_booking(
|
|||||||
db.rollback()
|
db.rollback()
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/me")
|
@router.get("/me")
|
||||||
async def get_my_service_bookings(
|
async def get_my_service_bookings(
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""Get all service bookings for current user"""
|
|
||||||
try:
|
try:
|
||||||
bookings = db.query(ServiceBooking).options(
|
bookings = db.query(ServiceBooking).options(
|
||||||
joinedload(ServiceBooking.service_items).joinedload(ServiceBookingItem.service)
|
joinedload(ServiceBooking.service_items).joinedload(ServiceBookingItem.service)
|
||||||
@@ -204,14 +198,12 @@ async def get_my_service_bookings(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{id}")
|
@router.get("/{id}")
|
||||||
async def get_service_booking_by_id(
|
async def get_service_booking_by_id(
|
||||||
id: int,
|
id: int,
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""Get service booking by ID"""
|
|
||||||
try:
|
try:
|
||||||
booking = db.query(ServiceBooking).options(
|
booking = db.query(ServiceBooking).options(
|
||||||
joinedload(ServiceBooking.service_items).joinedload(ServiceBookingItem.service)
|
joinedload(ServiceBooking.service_items).joinedload(ServiceBookingItem.service)
|
||||||
@@ -220,7 +212,7 @@ async def get_service_booking_by_id(
|
|||||||
if not booking:
|
if not booking:
|
||||||
raise HTTPException(status_code=404, detail="Service booking not found")
|
raise HTTPException(status_code=404, detail="Service booking not found")
|
||||||
|
|
||||||
# Check access
|
|
||||||
if booking.user_id != current_user.id and current_user.role_id != 1:
|
if booking.user_id != current_user.id and current_user.role_id != 1:
|
||||||
raise HTTPException(status_code=403, detail="Forbidden")
|
raise HTTPException(status_code=403, detail="Forbidden")
|
||||||
|
|
||||||
@@ -259,7 +251,6 @@ async def get_service_booking_by_id(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{id}/payment/stripe/create-intent")
|
@router.post("/{id}/payment/stripe/create-intent")
|
||||||
async def create_service_stripe_payment_intent(
|
async def create_service_stripe_payment_intent(
|
||||||
id: int,
|
id: int,
|
||||||
@@ -267,9 +258,8 @@ async def create_service_stripe_payment_intent(
|
|||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""Create Stripe payment intent for service booking"""
|
|
||||||
try:
|
try:
|
||||||
# Check if Stripe is configured
|
|
||||||
secret_key = get_stripe_secret_key(db)
|
secret_key = get_stripe_secret_key(db)
|
||||||
if not secret_key:
|
if not secret_key:
|
||||||
secret_key = settings.STRIPE_SECRET_KEY
|
secret_key = settings.STRIPE_SECRET_KEY
|
||||||
@@ -286,7 +276,7 @@ async def create_service_stripe_payment_intent(
|
|||||||
if amount <= 0:
|
if amount <= 0:
|
||||||
raise HTTPException(status_code=400, detail="Amount must be greater than 0")
|
raise HTTPException(status_code=400, detail="Amount must be greater than 0")
|
||||||
|
|
||||||
# Verify service booking exists and user has access
|
|
||||||
booking = db.query(ServiceBooking).filter(ServiceBooking.id == id).first()
|
booking = db.query(ServiceBooking).filter(ServiceBooking.id == id).first()
|
||||||
if not booking:
|
if not booking:
|
||||||
raise HTTPException(status_code=404, detail="Service booking not found")
|
raise HTTPException(status_code=404, detail="Service booking not found")
|
||||||
@@ -294,22 +284,22 @@ async def create_service_stripe_payment_intent(
|
|||||||
if booking.user_id != current_user.id and current_user.role_id != 1:
|
if booking.user_id != current_user.id and current_user.role_id != 1:
|
||||||
raise HTTPException(status_code=403, detail="Forbidden")
|
raise HTTPException(status_code=403, detail="Forbidden")
|
||||||
|
|
||||||
# Verify amount matches booking total
|
|
||||||
if abs(float(booking.total_amount) - amount) > 0.01:
|
if abs(float(booking.total_amount) - amount) > 0.01:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail=f"Amount mismatch. Booking total: {booking.total_amount}, Provided: {amount}"
|
detail=f"Amount mismatch. Booking total: {booking.total_amount}, Provided: {amount}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create payment intent
|
|
||||||
intent = StripeService.create_payment_intent(
|
intent = StripeService.create_payment_intent(
|
||||||
amount=amount,
|
amount=amount,
|
||||||
currency=currency,
|
currency=currency,
|
||||||
description=f"Service Booking #{booking.booking_number}",
|
description=f"Service Booking
|
||||||
db=db
|
db=db
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get publishable key
|
|
||||||
publishable_key = get_stripe_publishable_key(db)
|
publishable_key = get_stripe_publishable_key(db)
|
||||||
if not publishable_key:
|
if not publishable_key:
|
||||||
publishable_key = settings.STRIPE_PUBLISHABLE_KEY
|
publishable_key = settings.STRIPE_PUBLISHABLE_KEY
|
||||||
@@ -333,7 +323,6 @@ async def create_service_stripe_payment_intent(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{id}/payment/stripe/confirm")
|
@router.post("/{id}/payment/stripe/confirm")
|
||||||
async def confirm_service_stripe_payment(
|
async def confirm_service_stripe_payment(
|
||||||
id: int,
|
id: int,
|
||||||
@@ -341,14 +330,13 @@ async def confirm_service_stripe_payment(
|
|||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""Confirm Stripe payment for service booking"""
|
|
||||||
try:
|
try:
|
||||||
payment_intent_id = payment_data.get("payment_intent_id")
|
payment_intent_id = payment_data.get("payment_intent_id")
|
||||||
|
|
||||||
if not payment_intent_id:
|
if not payment_intent_id:
|
||||||
raise HTTPException(status_code=400, detail="payment_intent_id is required")
|
raise HTTPException(status_code=400, detail="payment_intent_id is required")
|
||||||
|
|
||||||
# Verify service booking exists and user has access
|
|
||||||
booking = db.query(ServiceBooking).filter(ServiceBooking.id == id).first()
|
booking = db.query(ServiceBooking).filter(ServiceBooking.id == id).first()
|
||||||
if not booking:
|
if not booking:
|
||||||
raise HTTPException(status_code=404, detail="Service booking not found")
|
raise HTTPException(status_code=404, detail="Service booking not found")
|
||||||
@@ -356,7 +344,7 @@ async def confirm_service_stripe_payment(
|
|||||||
if booking.user_id != current_user.id and current_user.role_id != 1:
|
if booking.user_id != current_user.id and current_user.role_id != 1:
|
||||||
raise HTTPException(status_code=403, detail="Forbidden")
|
raise HTTPException(status_code=403, detail="Forbidden")
|
||||||
|
|
||||||
# Retrieve and verify payment intent
|
|
||||||
intent_data = StripeService.retrieve_payment_intent(payment_intent_id, db)
|
intent_data = StripeService.retrieve_payment_intent(payment_intent_id, db)
|
||||||
|
|
||||||
if intent_data["status"] != "succeeded":
|
if intent_data["status"] != "succeeded":
|
||||||
@@ -365,15 +353,15 @@ async def confirm_service_stripe_payment(
|
|||||||
detail=f"Payment intent status is {intent_data['status']}, expected 'succeeded'"
|
detail=f"Payment intent status is {intent_data['status']}, expected 'succeeded'"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify amount matches
|
|
||||||
amount_paid = intent_data["amount"] / 100 # Convert from cents
|
amount_paid = intent_data["amount"] / 100
|
||||||
if abs(float(booking.total_amount) - amount_paid) > 0.01:
|
if abs(float(booking.total_amount) - amount_paid) > 0.01:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail="Payment amount does not match booking total"
|
detail="Payment amount does not match booking total"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create payment record
|
|
||||||
payment = ServicePayment(
|
payment = ServicePayment(
|
||||||
service_booking_id=booking.id,
|
service_booking_id=booking.id,
|
||||||
amount=booking.total_amount,
|
amount=booking.total_amount,
|
||||||
@@ -386,7 +374,7 @@ async def confirm_service_stripe_payment(
|
|||||||
|
|
||||||
db.add(payment)
|
db.add(payment)
|
||||||
|
|
||||||
# Update booking status
|
|
||||||
booking.status = ServiceBookingStatus.confirmed
|
booking.status = ServiceBookingStatus.confirmed
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
@@ -2,276 +2,133 @@ from fastapi import APIRouter, Depends, HTTPException, status, Query
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy import or_
|
from sqlalchemy import or_
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from ..config.database import get_db
|
from ..config.database import get_db
|
||||||
from ..middleware.auth import get_current_user, authorize_roles
|
from ..middleware.auth import get_current_user, authorize_roles
|
||||||
from ..models.user import User
|
from ..models.user import User
|
||||||
from ..models.service import Service
|
from ..models.service import Service
|
||||||
from ..models.service_usage import ServiceUsage
|
from ..models.service_usage import ServiceUsage
|
||||||
from ..models.booking import Booking, BookingStatus
|
from ..models.booking import Booking, BookingStatus
|
||||||
|
router = APIRouter(prefix='/services', tags=['services'])
|
||||||
|
|
||||||
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)):
|
||||||
|
|
||||||
@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:
|
try:
|
||||||
query = db.query(Service)
|
query = db.query(Service)
|
||||||
|
|
||||||
# Filter by search (name or description)
|
|
||||||
if search:
|
if search:
|
||||||
query = query.filter(
|
query = query.filter(or_(Service.name.like(f'%{search}%'), Service.description.like(f'%{search}%')))
|
||||||
or_(
|
|
||||||
Service.name.like(f"%{search}%"),
|
|
||||||
Service.description.like(f"%{search}%")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Filter by status (is_active)
|
|
||||||
if status_filter:
|
if status_filter:
|
||||||
is_active = status_filter == "active"
|
is_active = status_filter == 'active'
|
||||||
query = query.filter(Service.is_active == is_active)
|
query = query.filter(Service.is_active == is_active)
|
||||||
|
|
||||||
total = query.count()
|
total = query.count()
|
||||||
offset = (page - 1) * limit
|
offset = (page - 1) * limit
|
||||||
services = query.order_by(Service.created_at.desc()).offset(offset).limit(limit).all()
|
services = query.order_by(Service.created_at.desc()).offset(offset).limit(limit).all()
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for service in services:
|
for service in services:
|
||||||
service_dict = {
|
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}
|
||||||
"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)
|
result.append(service_dict)
|
||||||
|
return {'status': 'success', 'data': {'services': result, 'pagination': {'total': total, 'page': page, 'limit': limit, 'totalPages': (total + limit - 1) // limit}}}
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"data": {
|
|
||||||
"services": result,
|
|
||||||
"pagination": {
|
|
||||||
"total": total,
|
|
||||||
"page": page,
|
|
||||||
"limit": limit,
|
|
||||||
"totalPages": (total + limit - 1) // limit,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.get('/{id}')
|
||||||
@router.get("/{id}")
|
async def get_service_by_id(id: int, db: Session=Depends(get_db)):
|
||||||
async def get_service_by_id(id: int, db: Session = Depends(get_db)):
|
|
||||||
"""Get service by ID"""
|
|
||||||
try:
|
try:
|
||||||
service = db.query(Service).filter(Service.id == id).first()
|
service = db.query(Service).filter(Service.id == id).first()
|
||||||
if not service:
|
if not service:
|
||||||
raise HTTPException(status_code=404, detail="Service not found")
|
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}
|
||||||
service_dict = {
|
return {'status': 'success', 'data': {'service': 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:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.post('/', dependencies=[Depends(authorize_roles('admin'))])
|
||||||
@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)):
|
||||||
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:
|
try:
|
||||||
name = service_data.get("name")
|
name = service_data.get('name')
|
||||||
|
|
||||||
# Check if name exists
|
|
||||||
existing = db.query(Service).filter(Service.name == name).first()
|
existing = db.query(Service).filter(Service.name == name).first()
|
||||||
if existing:
|
if existing:
|
||||||
raise HTTPException(status_code=400, detail="Service name already exists")
|
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)
|
||||||
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.add(service)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(service)
|
db.refresh(service)
|
||||||
|
return {'status': 'success', 'message': 'Service created successfully', 'data': {'service': service}}
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"message": "Service created successfully",
|
|
||||||
"data": {"service": service}
|
|
||||||
}
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.put('/{id}', dependencies=[Depends(authorize_roles('admin'))])
|
||||||
@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)):
|
||||||
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:
|
try:
|
||||||
service = db.query(Service).filter(Service.id == id).first()
|
service = db.query(Service).filter(Service.id == id).first()
|
||||||
if not service:
|
if not service:
|
||||||
raise HTTPException(status_code=404, detail="Service not found")
|
raise HTTPException(status_code=404, detail='Service not found')
|
||||||
|
name = service_data.get('name')
|
||||||
# Check if new name exists (excluding current)
|
|
||||||
name = service_data.get("name")
|
|
||||||
if name and name != service.name:
|
if name and name != service.name:
|
||||||
existing = db.query(Service).filter(
|
existing = db.query(Service).filter(Service.name == name, Service.id != id).first()
|
||||||
Service.name == name,
|
|
||||||
Service.id != id
|
|
||||||
).first()
|
|
||||||
if existing:
|
if existing:
|
||||||
raise HTTPException(status_code=400, detail="Service name already exists")
|
raise HTTPException(status_code=400, detail='Service name already exists')
|
||||||
|
if 'name' in service_data:
|
||||||
# Update fields
|
service.name = service_data['name']
|
||||||
if "name" in service_data:
|
if 'description' in service_data:
|
||||||
service.name = service_data["name"]
|
service.description = service_data['description']
|
||||||
if "description" in service_data:
|
if 'price' in service_data:
|
||||||
service.description = service_data["description"]
|
service.price = float(service_data['price'])
|
||||||
if "price" in service_data:
|
if 'category' in service_data:
|
||||||
service.price = float(service_data["price"])
|
service.category = service_data['category']
|
||||||
if "category" in service_data:
|
if 'status' in service_data:
|
||||||
service.category = service_data["category"]
|
service.is_active = service_data['status'] == 'active'
|
||||||
if "status" in service_data:
|
|
||||||
service.is_active = service_data["status"] == "active"
|
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(service)
|
db.refresh(service)
|
||||||
|
return {'status': 'success', 'message': 'Service updated successfully', 'data': {'service': service}}
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"message": "Service updated successfully",
|
|
||||||
"data": {"service": service}
|
|
||||||
}
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.delete('/{id}', dependencies=[Depends(authorize_roles('admin'))])
|
||||||
@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)):
|
||||||
async def delete_service(
|
|
||||||
id: int,
|
|
||||||
current_user: User = Depends(authorize_roles("admin")),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Delete service (Admin only)"""
|
|
||||||
try:
|
try:
|
||||||
service = db.query(Service).filter(Service.id == id).first()
|
service = db.query(Service).filter(Service.id == id).first()
|
||||||
if not service:
|
if not service:
|
||||||
raise HTTPException(status_code=404, detail="Service not found")
|
raise HTTPException(status_code=404, detail='Service not found')
|
||||||
|
active_usage = db.query(ServiceUsage).join(Booking).filter(ServiceUsage.service_id == id, Booking.status.in_([BookingStatus.pending, BookingStatus.confirmed, BookingStatus.checked_in])).count()
|
||||||
# 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:
|
if active_usage > 0:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=400, detail='Cannot delete service that is used in active bookings')
|
||||||
status_code=400,
|
|
||||||
detail="Cannot delete service that is used in active bookings"
|
|
||||||
)
|
|
||||||
|
|
||||||
db.delete(service)
|
db.delete(service)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
return {'status': 'success', 'message': 'Service deleted successfully'}
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"message": "Service deleted successfully"
|
|
||||||
}
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.post('/use')
|
||||||
@router.post("/use")
|
async def use_service(usage_data: dict, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||||
async def use_service(
|
|
||||||
usage_data: dict,
|
|
||||||
current_user: User = Depends(get_current_user),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Add service to booking"""
|
|
||||||
try:
|
try:
|
||||||
booking_id = usage_data.get("booking_id")
|
booking_id = usage_data.get('booking_id')
|
||||||
service_id = usage_data.get("service_id")
|
service_id = usage_data.get('service_id')
|
||||||
quantity = usage_data.get("quantity", 1)
|
quantity = usage_data.get('quantity', 1)
|
||||||
|
|
||||||
# Check if booking exists
|
|
||||||
booking = db.query(Booking).filter(Booking.id == booking_id).first()
|
booking = db.query(Booking).filter(Booking.id == booking_id).first()
|
||||||
if not booking:
|
if not booking:
|
||||||
raise HTTPException(status_code=404, detail="Booking not found")
|
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()
|
service = db.query(Service).filter(Service.id == service_id).first()
|
||||||
if not service or not service.is_active:
|
if not service or not service.is_active:
|
||||||
raise HTTPException(status_code=404, detail="Service not found or inactive")
|
raise HTTPException(status_code=404, detail='Service not found or inactive')
|
||||||
|
|
||||||
# Calculate total price
|
|
||||||
total_price = float(service.price) * quantity
|
total_price = float(service.price) * quantity
|
||||||
|
service_usage = ServiceUsage(booking_id=booking_id, service_id=service_id, quantity=quantity, unit_price=service.price, total_price=total_price)
|
||||||
# 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.add(service_usage)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(service_usage)
|
db.refresh(service_usage)
|
||||||
|
return {'status': 'success', 'message': 'Service added to booking successfully', 'data': {'bookingService': service_usage}}
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"message": "Service added to booking successfully",
|
|
||||||
"data": {"bookingService": service_usage}
|
|
||||||
}
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
@@ -16,9 +16,7 @@ from ..models.system_settings import SystemSettings
|
|||||||
from ..utils.mailer import send_email
|
from ..utils.mailer import send_email
|
||||||
from ..services.room_service import get_base_url
|
from ..services.room_service import get_base_url
|
||||||
|
|
||||||
|
|
||||||
def normalize_image_url(image_url: str, base_url: str) -> str:
|
def normalize_image_url(image_url: str, base_url: str) -> str:
|
||||||
"""Normalize image URL to absolute URL"""
|
|
||||||
if not image_url:
|
if not image_url:
|
||||||
return image_url
|
return image_url
|
||||||
if image_url.startswith('http://') or image_url.startswith('https://'):
|
if image_url.startswith('http://') or image_url.startswith('https://'):
|
||||||
@@ -31,19 +29,17 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
router = APIRouter(prefix="/admin/system-settings", tags=["admin-system-settings"])
|
router = APIRouter(prefix="/admin/system-settings", tags=["admin-system-settings"])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/currency")
|
@router.get("/currency")
|
||||||
async def get_platform_currency(
|
async def get_platform_currency(
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""Get platform currency setting (public endpoint for frontend)"""
|
|
||||||
try:
|
try:
|
||||||
setting = db.query(SystemSettings).filter(
|
setting = db.query(SystemSettings).filter(
|
||||||
SystemSettings.key == "platform_currency"
|
SystemSettings.key == "platform_currency"
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
if not setting:
|
if not setting:
|
||||||
# Default to VND if not set
|
|
||||||
return {
|
return {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"data": {
|
"data": {
|
||||||
@@ -64,25 +60,23 @@ async def get_platform_currency(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.put("/currency")
|
@router.put("/currency")
|
||||||
async def update_platform_currency(
|
async def update_platform_currency(
|
||||||
currency_data: dict,
|
currency_data: dict,
|
||||||
current_user: User = Depends(authorize_roles("admin")),
|
current_user: User = Depends(authorize_roles("admin")),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""Update platform currency (Admin only)"""
|
|
||||||
try:
|
try:
|
||||||
currency = currency_data.get("currency", "").upper()
|
currency = currency_data.get("currency", "").upper()
|
||||||
|
|
||||||
# Validate currency code
|
|
||||||
if not currency or len(currency) != 3 or not currency.isalpha():
|
if not currency or len(currency) != 3 or not currency.isalpha():
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail="Invalid currency code. Must be a 3-letter ISO 4217 code (e.g., USD, EUR, VND)"
|
detail="Invalid currency code. Must be a 3-letter ISO 4217 code (e.g., USD, EUR, VND)"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get or create setting
|
|
||||||
setting = db.query(SystemSettings).filter(
|
setting = db.query(SystemSettings).filter(
|
||||||
SystemSettings.key == "platform_currency"
|
SystemSettings.key == "platform_currency"
|
||||||
).first()
|
).first()
|
||||||
@@ -117,13 +111,11 @@ async def update_platform_currency(
|
|||||||
db.rollback()
|
db.rollback()
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/")
|
@router.get("/")
|
||||||
async def get_all_settings(
|
async def get_all_settings(
|
||||||
current_user: User = Depends(authorize_roles("admin")),
|
current_user: User = Depends(authorize_roles("admin")),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""Get all system settings (Admin only)"""
|
|
||||||
try:
|
try:
|
||||||
settings = db.query(SystemSettings).all()
|
settings = db.query(SystemSettings).all()
|
||||||
|
|
||||||
@@ -146,13 +138,11 @@ async def get_all_settings(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/stripe")
|
@router.get("/stripe")
|
||||||
async def get_stripe_settings(
|
async def get_stripe_settings(
|
||||||
current_user: User = Depends(authorize_roles("admin")),
|
current_user: User = Depends(authorize_roles("admin")),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""Get Stripe payment settings (Admin only)"""
|
|
||||||
try:
|
try:
|
||||||
secret_key_setting = db.query(SystemSettings).filter(
|
secret_key_setting = db.query(SystemSettings).filter(
|
||||||
SystemSettings.key == "stripe_secret_key"
|
SystemSettings.key == "stripe_secret_key"
|
||||||
@@ -166,7 +156,7 @@ async def get_stripe_settings(
|
|||||||
SystemSettings.key == "stripe_webhook_secret"
|
SystemSettings.key == "stripe_webhook_secret"
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
# Mask secret keys for security (only show last 4 characters)
|
|
||||||
def mask_key(key_value: str) -> str:
|
def mask_key(key_value: str) -> str:
|
||||||
if not key_value or len(key_value) < 4:
|
if not key_value or len(key_value) < 4:
|
||||||
return ""
|
return ""
|
||||||
@@ -206,41 +196,39 @@ async def get_stripe_settings(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.put("/stripe")
|
@router.put("/stripe")
|
||||||
async def update_stripe_settings(
|
async def update_stripe_settings(
|
||||||
stripe_data: dict,
|
stripe_data: dict,
|
||||||
current_user: User = Depends(authorize_roles("admin")),
|
current_user: User = Depends(authorize_roles("admin")),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""Update Stripe payment settings (Admin only)"""
|
|
||||||
try:
|
try:
|
||||||
secret_key = stripe_data.get("stripe_secret_key", "").strip()
|
secret_key = stripe_data.get("stripe_secret_key", "").strip()
|
||||||
publishable_key = stripe_data.get("stripe_publishable_key", "").strip()
|
publishable_key = stripe_data.get("stripe_publishable_key", "").strip()
|
||||||
webhook_secret = stripe_data.get("stripe_webhook_secret", "").strip()
|
webhook_secret = stripe_data.get("stripe_webhook_secret", "").strip()
|
||||||
|
|
||||||
# Validate secret key format (should start with sk_)
|
|
||||||
if secret_key and not secret_key.startswith("sk_"):
|
if secret_key and not secret_key.startswith("sk_"):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail="Invalid Stripe secret key format. Must start with 'sk_'"
|
detail="Invalid Stripe secret key format. Must start with 'sk_'"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate publishable key format (should start with pk_)
|
|
||||||
if publishable_key and not publishable_key.startswith("pk_"):
|
if publishable_key and not publishable_key.startswith("pk_"):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail="Invalid Stripe publishable key format. Must start with 'pk_'"
|
detail="Invalid Stripe publishable key format. Must start with 'pk_'"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate webhook secret format (should start with whsec_)
|
|
||||||
if webhook_secret and not webhook_secret.startswith("whsec_"):
|
if webhook_secret and not webhook_secret.startswith("whsec_"):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail="Invalid Stripe webhook secret format. Must start with 'whsec_'"
|
detail="Invalid Stripe webhook secret format. Must start with 'whsec_'"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update or create secret key setting
|
|
||||||
if secret_key:
|
if secret_key:
|
||||||
setting = db.query(SystemSettings).filter(
|
setting = db.query(SystemSettings).filter(
|
||||||
SystemSettings.key == "stripe_secret_key"
|
SystemSettings.key == "stripe_secret_key"
|
||||||
@@ -258,7 +246,7 @@ async def update_stripe_settings(
|
|||||||
)
|
)
|
||||||
db.add(setting)
|
db.add(setting)
|
||||||
|
|
||||||
# Update or create publishable key setting
|
|
||||||
if publishable_key:
|
if publishable_key:
|
||||||
setting = db.query(SystemSettings).filter(
|
setting = db.query(SystemSettings).filter(
|
||||||
SystemSettings.key == "stripe_publishable_key"
|
SystemSettings.key == "stripe_publishable_key"
|
||||||
@@ -276,7 +264,7 @@ async def update_stripe_settings(
|
|||||||
)
|
)
|
||||||
db.add(setting)
|
db.add(setting)
|
||||||
|
|
||||||
# Update or create webhook secret setting
|
|
||||||
if webhook_secret:
|
if webhook_secret:
|
||||||
setting = db.query(SystemSettings).filter(
|
setting = db.query(SystemSettings).filter(
|
||||||
SystemSettings.key == "stripe_webhook_secret"
|
SystemSettings.key == "stripe_webhook_secret"
|
||||||
@@ -296,7 +284,7 @@ async def update_stripe_settings(
|
|||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
# Return masked values
|
|
||||||
def mask_key(key_value: str) -> str:
|
def mask_key(key_value: str) -> str:
|
||||||
if not key_value or len(key_value) < 4:
|
if not key_value or len(key_value) < 4:
|
||||||
return ""
|
return ""
|
||||||
@@ -322,13 +310,11 @@ async def update_stripe_settings(
|
|||||||
db.rollback()
|
db.rollback()
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/paypal")
|
@router.get("/paypal")
|
||||||
async def get_paypal_settings(
|
async def get_paypal_settings(
|
||||||
current_user: User = Depends(authorize_roles("admin")),
|
current_user: User = Depends(authorize_roles("admin")),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""Get PayPal payment settings (Admin only)"""
|
|
||||||
try:
|
try:
|
||||||
client_id_setting = db.query(SystemSettings).filter(
|
client_id_setting = db.query(SystemSettings).filter(
|
||||||
SystemSettings.key == "paypal_client_id"
|
SystemSettings.key == "paypal_client_id"
|
||||||
@@ -342,7 +328,7 @@ async def get_paypal_settings(
|
|||||||
SystemSettings.key == "paypal_mode"
|
SystemSettings.key == "paypal_mode"
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
# Mask secret for security (only show last 4 characters)
|
|
||||||
def mask_key(key_value: str) -> str:
|
def mask_key(key_value: str) -> str:
|
||||||
if not key_value or len(key_value) < 4:
|
if not key_value or len(key_value) < 4:
|
||||||
return ""
|
return ""
|
||||||
@@ -378,27 +364,25 @@ async def get_paypal_settings(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.put("/paypal")
|
@router.put("/paypal")
|
||||||
async def update_paypal_settings(
|
async def update_paypal_settings(
|
||||||
paypal_data: dict,
|
paypal_data: dict,
|
||||||
current_user: User = Depends(authorize_roles("admin")),
|
current_user: User = Depends(authorize_roles("admin")),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""Update PayPal payment settings (Admin only)"""
|
|
||||||
try:
|
try:
|
||||||
client_id = paypal_data.get("paypal_client_id", "").strip()
|
client_id = paypal_data.get("paypal_client_id", "").strip()
|
||||||
client_secret = paypal_data.get("paypal_client_secret", "").strip()
|
client_secret = paypal_data.get("paypal_client_secret", "").strip()
|
||||||
mode = paypal_data.get("paypal_mode", "sandbox").strip().lower()
|
mode = paypal_data.get("paypal_mode", "sandbox").strip().lower()
|
||||||
|
|
||||||
# Validate mode
|
|
||||||
if mode and mode not in ["sandbox", "live"]:
|
if mode and mode not in ["sandbox", "live"]:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail="Invalid PayPal mode. Must be 'sandbox' or 'live'"
|
detail="Invalid PayPal mode. Must be 'sandbox' or 'live'"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update or create client ID setting
|
|
||||||
if client_id:
|
if client_id:
|
||||||
setting = db.query(SystemSettings).filter(
|
setting = db.query(SystemSettings).filter(
|
||||||
SystemSettings.key == "paypal_client_id"
|
SystemSettings.key == "paypal_client_id"
|
||||||
@@ -416,7 +400,7 @@ async def update_paypal_settings(
|
|||||||
)
|
)
|
||||||
db.add(setting)
|
db.add(setting)
|
||||||
|
|
||||||
# Update or create client secret setting
|
|
||||||
if client_secret:
|
if client_secret:
|
||||||
setting = db.query(SystemSettings).filter(
|
setting = db.query(SystemSettings).filter(
|
||||||
SystemSettings.key == "paypal_client_secret"
|
SystemSettings.key == "paypal_client_secret"
|
||||||
@@ -434,7 +418,7 @@ async def update_paypal_settings(
|
|||||||
)
|
)
|
||||||
db.add(setting)
|
db.add(setting)
|
||||||
|
|
||||||
# Update or create mode setting
|
|
||||||
if mode:
|
if mode:
|
||||||
setting = db.query(SystemSettings).filter(
|
setting = db.query(SystemSettings).filter(
|
||||||
SystemSettings.key == "paypal_mode"
|
SystemSettings.key == "paypal_mode"
|
||||||
@@ -454,7 +438,7 @@ async def update_paypal_settings(
|
|||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
# Return masked values
|
|
||||||
def mask_key(key_value: str) -> str:
|
def mask_key(key_value: str) -> str:
|
||||||
if not key_value or len(key_value) < 4:
|
if not key_value or len(key_value) < 4:
|
||||||
return ""
|
return ""
|
||||||
@@ -478,15 +462,13 @@ async def update_paypal_settings(
|
|||||||
db.rollback()
|
db.rollback()
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/smtp")
|
@router.get("/smtp")
|
||||||
async def get_smtp_settings(
|
async def get_smtp_settings(
|
||||||
current_user: User = Depends(authorize_roles("admin")),
|
current_user: User = Depends(authorize_roles("admin")),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""Get SMTP email server settings (Admin only)"""
|
|
||||||
try:
|
try:
|
||||||
# Get all SMTP settings
|
|
||||||
smtp_settings = {}
|
smtp_settings = {}
|
||||||
setting_keys = [
|
setting_keys = [
|
||||||
"smtp_host",
|
"smtp_host",
|
||||||
@@ -505,7 +487,7 @@ async def get_smtp_settings(
|
|||||||
if setting:
|
if setting:
|
||||||
smtp_settings[key] = setting.value
|
smtp_settings[key] = setting.value
|
||||||
|
|
||||||
# Mask password for security (only show last 4 characters if set)
|
|
||||||
def mask_password(password_value: str) -> str:
|
def mask_password(password_value: str) -> str:
|
||||||
if not password_value or len(password_value) < 4:
|
if not password_value or len(password_value) < 4:
|
||||||
return ""
|
return ""
|
||||||
@@ -525,7 +507,7 @@ async def get_smtp_settings(
|
|||||||
"has_password": bool(smtp_settings.get("smtp_password")),
|
"has_password": bool(smtp_settings.get("smtp_password")),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get updated_at and updated_by from any setting (prefer password setting if exists)
|
|
||||||
password_setting = db.query(SystemSettings).filter(
|
password_setting = db.query(SystemSettings).filter(
|
||||||
SystemSettings.key == "smtp_password"
|
SystemSettings.key == "smtp_password"
|
||||||
).first()
|
).first()
|
||||||
@@ -534,7 +516,7 @@ async def get_smtp_settings(
|
|||||||
result["updated_at"] = password_setting.updated_at.isoformat() if password_setting.updated_at else None
|
result["updated_at"] = password_setting.updated_at.isoformat() if password_setting.updated_at else None
|
||||||
result["updated_by"] = password_setting.updated_by.full_name if password_setting.updated_by else None
|
result["updated_by"] = password_setting.updated_by.full_name if password_setting.updated_by else None
|
||||||
else:
|
else:
|
||||||
# Try to get from any other SMTP setting
|
|
||||||
any_setting = db.query(SystemSettings).filter(
|
any_setting = db.query(SystemSettings).filter(
|
||||||
SystemSettings.key.in_(setting_keys)
|
SystemSettings.key.in_(setting_keys)
|
||||||
).first()
|
).first()
|
||||||
@@ -552,14 +534,12 @@ async def get_smtp_settings(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.put("/smtp")
|
@router.put("/smtp")
|
||||||
async def update_smtp_settings(
|
async def update_smtp_settings(
|
||||||
smtp_data: dict,
|
smtp_data: dict,
|
||||||
current_user: User = Depends(authorize_roles("admin")),
|
current_user: User = Depends(authorize_roles("admin")),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""Update SMTP email server settings (Admin only)"""
|
|
||||||
try:
|
try:
|
||||||
smtp_host = smtp_data.get("smtp_host", "").strip()
|
smtp_host = smtp_data.get("smtp_host", "").strip()
|
||||||
smtp_port = smtp_data.get("smtp_port", "").strip()
|
smtp_port = smtp_data.get("smtp_port", "").strip()
|
||||||
@@ -569,7 +549,7 @@ async def update_smtp_settings(
|
|||||||
smtp_from_name = smtp_data.get("smtp_from_name", "").strip()
|
smtp_from_name = smtp_data.get("smtp_from_name", "").strip()
|
||||||
smtp_use_tls = smtp_data.get("smtp_use_tls", True)
|
smtp_use_tls = smtp_data.get("smtp_use_tls", True)
|
||||||
|
|
||||||
# Validate required fields if provided
|
|
||||||
if smtp_host and not smtp_host:
|
if smtp_host and not smtp_host:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
@@ -591,14 +571,14 @@ async def update_smtp_settings(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if smtp_from_email:
|
if smtp_from_email:
|
||||||
# Basic email validation
|
|
||||||
if "@" not in smtp_from_email or "." not in smtp_from_email.split("@")[1]:
|
if "@" not in smtp_from_email or "." not in smtp_from_email.split("@")[1]:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail="Invalid email address format for 'From Email'"
|
detail="Invalid email address format for 'From Email'"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Helper function to update or create setting
|
|
||||||
def update_setting(key: str, value: str, description: str):
|
def update_setting(key: str, value: str, description: str):
|
||||||
setting = db.query(SystemSettings).filter(
|
setting = db.query(SystemSettings).filter(
|
||||||
SystemSettings.key == key
|
SystemSettings.key == key
|
||||||
@@ -616,7 +596,7 @@ async def update_smtp_settings(
|
|||||||
)
|
)
|
||||||
db.add(setting)
|
db.add(setting)
|
||||||
|
|
||||||
# Update or create settings (only update if value is provided)
|
|
||||||
if smtp_host:
|
if smtp_host:
|
||||||
update_setting(
|
update_setting(
|
||||||
"smtp_host",
|
"smtp_host",
|
||||||
@@ -659,7 +639,7 @@ async def update_smtp_settings(
|
|||||||
"Default 'From' name for outgoing emails"
|
"Default 'From' name for outgoing emails"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update TLS setting (convert boolean to string)
|
|
||||||
if smtp_use_tls is not None:
|
if smtp_use_tls is not None:
|
||||||
update_setting(
|
update_setting(
|
||||||
"smtp_use_tls",
|
"smtp_use_tls",
|
||||||
@@ -669,13 +649,13 @@ async def update_smtp_settings(
|
|||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
# Return updated settings with masked password
|
|
||||||
def mask_password(password_value: str) -> str:
|
def mask_password(password_value: str) -> str:
|
||||||
if not password_value or len(password_value) < 4:
|
if not password_value or len(password_value) < 4:
|
||||||
return ""
|
return ""
|
||||||
return "*" * (len(password_value) - 4) + password_value[-4:]
|
return "*" * (len(password_value) - 4) + password_value[-4:]
|
||||||
|
|
||||||
# Get updated settings
|
|
||||||
updated_settings = {}
|
updated_settings = {}
|
||||||
for key in ["smtp_host", "smtp_port", "smtp_user", "smtp_password", "smtp_from_email", "smtp_from_name", "smtp_use_tls"]:
|
for key in ["smtp_host", "smtp_port", "smtp_user", "smtp_password", "smtp_from_email", "smtp_from_name", "smtp_use_tls"]:
|
||||||
setting = db.query(SystemSettings).filter(
|
setting = db.query(SystemSettings).filter(
|
||||||
@@ -698,7 +678,7 @@ async def update_smtp_settings(
|
|||||||
"has_password": bool(updated_settings.get("smtp_password")),
|
"has_password": bool(updated_settings.get("smtp_password")),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get updated_by from password setting if it exists
|
|
||||||
password_setting = db.query(SystemSettings).filter(
|
password_setting = db.query(SystemSettings).filter(
|
||||||
SystemSettings.key == "smtp_password"
|
SystemSettings.key == "smtp_password"
|
||||||
).first()
|
).first()
|
||||||
@@ -717,131 +697,28 @@ async def update_smtp_settings(
|
|||||||
db.rollback()
|
db.rollback()
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
class TestEmailRequest(BaseModel):
|
class TestEmailRequest(BaseModel):
|
||||||
email: EmailStr
|
email: EmailStr
|
||||||
|
|
||||||
|
|
||||||
@router.post("/smtp/test")
|
@router.post("/smtp/test")
|
||||||
async def test_smtp_email(
|
async def test_smtp_email(
|
||||||
request: TestEmailRequest,
|
request: TestEmailRequest,
|
||||||
current_user: User = Depends(authorize_roles("admin")),
|
current_user: User = Depends(authorize_roles("admin")),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""Send a test email to verify SMTP settings (Admin only)"""
|
|
||||||
try:
|
try:
|
||||||
test_email = str(request.email)
|
test_email = str(request.email)
|
||||||
admin_name = str(current_user.full_name or current_user.email or "Admin")
|
admin_name = str(current_user.full_name or current_user.email or "Admin")
|
||||||
timestamp_str = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC")
|
timestamp_str = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||||
|
|
||||||
# Create test email HTML content
|
|
||||||
test_html = f"""
|
test_html = f
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<style>
|
|
||||||
body {{
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #333;
|
|
||||||
max-width: 600px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
}}
|
|
||||||
.header {{
|
|
||||||
background: linear-gradient(135deg, #d4af37 0%, #c9a227 100%);
|
|
||||||
color: white;
|
|
||||||
padding: 30px;
|
|
||||||
text-align: center;
|
|
||||||
border-radius: 10px 10px 0 0;
|
|
||||||
}}
|
|
||||||
.content {{
|
|
||||||
background: #f9f9f9;
|
|
||||||
padding: 30px;
|
|
||||||
border: 1px solid #e0e0e0;
|
|
||||||
border-radius: 0 0 10px 10px;
|
|
||||||
}}
|
|
||||||
.success-icon {{
|
|
||||||
font-size: 48px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}}
|
|
||||||
.info-box {{
|
|
||||||
background: white;
|
|
||||||
padding: 20px;
|
|
||||||
margin: 20px 0;
|
|
||||||
border-left: 4px solid #d4af37;
|
|
||||||
border-radius: 5px;
|
|
||||||
}}
|
|
||||||
.footer {{
|
|
||||||
margin-top: 30px;
|
|
||||||
padding-top: 20px;
|
|
||||||
border-top: 1px solid #e0e0e0;
|
|
||||||
text-align: center;
|
|
||||||
color: #666;
|
|
||||||
font-size: 12px;
|
|
||||||
}}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="header">
|
|
||||||
<h1>✅ SMTP Test Email</h1>
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<div style="text-align: center;">
|
|
||||||
<div class="success-icon">🎉</div>
|
|
||||||
<h2>Email Configuration Test Successful!</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>This is a test email sent from your Hotel Booking system to verify that the SMTP email settings are configured correctly.</p>
|
|
||||||
|
|
||||||
<div class="info-box">
|
|
||||||
<strong>📧 Test Details:</strong>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Recipient:</strong> {test_email}</li>
|
|
||||||
<li><strong>Sent by:</strong> {admin_name}</li>
|
|
||||||
<li><strong>Time:</strong> {timestamp_str}</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>If you received this email, it means your SMTP server settings are working correctly and the system can send emails through your configured email server.</p>
|
|
||||||
|
|
||||||
<p><strong>What's next?</strong></p>
|
|
||||||
<ul>
|
|
||||||
<li>Welcome emails for new user registrations</li>
|
|
||||||
<li>Password reset emails</li>
|
|
||||||
<li>Booking confirmation emails</li>
|
|
||||||
<li>Payment notifications</li>
|
|
||||||
<li>And other system notifications</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div class="footer">
|
|
||||||
<p>This is an automated test email from Hotel Booking System</p>
|
|
||||||
<p>If you did not request this test, please ignore this email.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Plain text version
|
|
||||||
test_text = f"""
|
|
||||||
SMTP Test Email
|
|
||||||
|
|
||||||
This is a test email sent from your Hotel Booking system to verify that the SMTP email settings are configured correctly.
|
test_text = f
|
||||||
|
.strip()
|
||||||
Test Details:
|
|
||||||
- Recipient: {test_email}
|
|
||||||
- Sent by: {admin_name}
|
|
||||||
- Time: {timestamp_str}
|
|
||||||
|
|
||||||
If you received this email, it means your SMTP server settings are working correctly and the system can send emails through your configured email server.
|
|
||||||
|
|
||||||
This is an automated test email from Hotel Booking System
|
|
||||||
If you did not request this test, please ignore this email.
|
|
||||||
""".strip()
|
|
||||||
|
|
||||||
# Send the test email
|
|
||||||
await send_email(
|
await send_email(
|
||||||
to=test_email,
|
to=test_email,
|
||||||
subject="SMTP Test Email - Hotel Booking System",
|
subject="SMTP Test Email - Hotel Booking System",
|
||||||
@@ -860,13 +737,13 @@ If you did not request this test, please ignore this email.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
# Re-raise HTTP exceptions (like validation errors from send_email)
|
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = str(e)
|
error_msg = str(e)
|
||||||
logger.error(f"Error sending test email: {type(e).__name__}: {error_msg}", exc_info=True)
|
logger.error(f"Error sending test email: {type(e).__name__}: {error_msg}", exc_info=True)
|
||||||
|
|
||||||
# Provide more user-friendly error messages
|
|
||||||
if "SMTP mailer not configured" in error_msg:
|
if "SMTP mailer not configured" in error_msg:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
@@ -888,7 +765,6 @@ If you did not request this test, please ignore this email.
|
|||||||
detail=f"Failed to send test email: {error_msg}"
|
detail=f"Failed to send test email: {error_msg}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class UpdateCompanySettingsRequest(BaseModel):
|
class UpdateCompanySettingsRequest(BaseModel):
|
||||||
company_name: Optional[str] = None
|
company_name: Optional[str] = None
|
||||||
company_tagline: Optional[str] = None
|
company_tagline: Optional[str] = None
|
||||||
@@ -897,12 +773,10 @@ class UpdateCompanySettingsRequest(BaseModel):
|
|||||||
company_address: Optional[str] = None
|
company_address: Optional[str] = None
|
||||||
tax_rate: Optional[float] = None
|
tax_rate: Optional[float] = None
|
||||||
|
|
||||||
|
|
||||||
@router.get("/company")
|
@router.get("/company")
|
||||||
async def get_company_settings(
|
async def get_company_settings(
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""Get company settings (public endpoint for frontend)"""
|
|
||||||
try:
|
try:
|
||||||
setting_keys = [
|
setting_keys = [
|
||||||
"company_name",
|
"company_name",
|
||||||
@@ -925,7 +799,7 @@ async def get_company_settings(
|
|||||||
else:
|
else:
|
||||||
settings_dict[key] = None
|
settings_dict[key] = None
|
||||||
|
|
||||||
# Get updated_at and updated_by from logo setting if exists
|
|
||||||
logo_setting = db.query(SystemSettings).filter(
|
logo_setting = db.query(SystemSettings).filter(
|
||||||
SystemSettings.key == "company_logo_url"
|
SystemSettings.key == "company_logo_url"
|
||||||
).first()
|
).first()
|
||||||
@@ -954,14 +828,12 @@ async def get_company_settings(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.put("/company")
|
@router.put("/company")
|
||||||
async def update_company_settings(
|
async def update_company_settings(
|
||||||
request_data: UpdateCompanySettingsRequest,
|
request_data: UpdateCompanySettingsRequest,
|
||||||
current_user: User = Depends(authorize_roles("admin")),
|
current_user: User = Depends(authorize_roles("admin")),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""Update company settings (Admin only)"""
|
|
||||||
try:
|
try:
|
||||||
db_settings = {}
|
db_settings = {}
|
||||||
|
|
||||||
@@ -979,18 +851,18 @@ async def update_company_settings(
|
|||||||
db_settings["tax_rate"] = str(request_data.tax_rate)
|
db_settings["tax_rate"] = str(request_data.tax_rate)
|
||||||
|
|
||||||
for key, value in db_settings.items():
|
for key, value in db_settings.items():
|
||||||
# Find or create setting
|
|
||||||
setting = db.query(SystemSettings).filter(
|
setting = db.query(SystemSettings).filter(
|
||||||
SystemSettings.key == key
|
SystemSettings.key == key
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
if setting:
|
if setting:
|
||||||
# Update existing
|
|
||||||
setting.value = value if value else None
|
setting.value = value if value else None
|
||||||
setting.updated_at = datetime.utcnow()
|
setting.updated_at = datetime.utcnow()
|
||||||
setting.updated_by_id = current_user.id
|
setting.updated_by_id = current_user.id
|
||||||
else:
|
else:
|
||||||
# Create new
|
|
||||||
setting = SystemSettings(
|
setting = SystemSettings(
|
||||||
key=key,
|
key=key,
|
||||||
value=value if value else None,
|
value=value if value else None,
|
||||||
@@ -1000,7 +872,7 @@ async def update_company_settings(
|
|||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
# Get updated settings
|
|
||||||
updated_settings = {}
|
updated_settings = {}
|
||||||
for key in ["company_name", "company_tagline", "company_logo_url", "company_favicon_url", "company_phone", "company_email", "company_address", "tax_rate"]:
|
for key in ["company_name", "company_tagline", "company_logo_url", "company_favicon_url", "company_phone", "company_email", "company_address", "tax_rate"]:
|
||||||
setting = db.query(SystemSettings).filter(
|
setting = db.query(SystemSettings).filter(
|
||||||
@@ -1011,7 +883,7 @@ async def update_company_settings(
|
|||||||
else:
|
else:
|
||||||
updated_settings[key] = None
|
updated_settings[key] = None
|
||||||
|
|
||||||
# Get updated_at and updated_by
|
|
||||||
logo_setting = db.query(SystemSettings).filter(
|
logo_setting = db.query(SystemSettings).filter(
|
||||||
SystemSettings.key == "company_logo_url"
|
SystemSettings.key == "company_logo_url"
|
||||||
).first()
|
).first()
|
||||||
@@ -1048,7 +920,6 @@ async def update_company_settings(
|
|||||||
db.rollback()
|
db.rollback()
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.post("/company/logo")
|
@router.post("/company/logo")
|
||||||
async def upload_company_logo(
|
async def upload_company_logo(
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -1056,28 +927,27 @@ async def upload_company_logo(
|
|||||||
current_user: User = Depends(authorize_roles("admin")),
|
current_user: User = Depends(authorize_roles("admin")),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""Upload company logo (Admin only)"""
|
|
||||||
try:
|
try:
|
||||||
# Validate file type
|
|
||||||
if not image.content_type or not image.content_type.startswith('image/'):
|
if not image.content_type or not image.content_type.startswith('image/'):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="File must be an image"
|
detail="File must be an image"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate file size (max 2MB)
|
|
||||||
content = await image.read()
|
content = await image.read()
|
||||||
if len(content) > 2 * 1024 * 1024: # 2MB
|
if len(content) > 2 * 1024 * 1024:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Logo file size must be less than 2MB"
|
detail="Logo file size must be less than 2MB"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create uploads directory
|
|
||||||
upload_dir = Path(__file__).parent.parent.parent / "uploads" / "company"
|
upload_dir = Path(__file__).parent.parent.parent / "uploads" / "company"
|
||||||
upload_dir.mkdir(parents=True, exist_ok=True)
|
upload_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Delete old logo if exists
|
|
||||||
old_logo_setting = db.query(SystemSettings).filter(
|
old_logo_setting = db.query(SystemSettings).filter(
|
||||||
SystemSettings.key == "company_logo_url"
|
SystemSettings.key == "company_logo_url"
|
||||||
).first()
|
).first()
|
||||||
@@ -1090,20 +960,20 @@ async def upload_company_logo(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Could not delete old logo: {e}")
|
logger.warning(f"Could not delete old logo: {e}")
|
||||||
|
|
||||||
# Generate filename
|
|
||||||
ext = Path(image.filename).suffix or '.png'
|
ext = Path(image.filename).suffix or '.png'
|
||||||
# Always use logo.png to ensure we only have one logo
|
|
||||||
filename = "logo.png"
|
filename = "logo.png"
|
||||||
file_path = upload_dir / filename
|
file_path = upload_dir / filename
|
||||||
|
|
||||||
# Save file
|
|
||||||
async with aiofiles.open(file_path, 'wb') as f:
|
async with aiofiles.open(file_path, 'wb') as f:
|
||||||
await f.write(content)
|
await f.write(content)
|
||||||
|
|
||||||
# Store the URL in system_settings
|
|
||||||
image_url = f"/uploads/company/{filename}"
|
image_url = f"/uploads/company/{filename}"
|
||||||
|
|
||||||
# Update or create setting
|
|
||||||
logo_setting = db.query(SystemSettings).filter(
|
logo_setting = db.query(SystemSettings).filter(
|
||||||
SystemSettings.key == "company_logo_url"
|
SystemSettings.key == "company_logo_url"
|
||||||
).first()
|
).first()
|
||||||
@@ -1122,7 +992,7 @@ async def upload_company_logo(
|
|||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
# Return the image URL
|
|
||||||
base_url = get_base_url(request)
|
base_url = get_base_url(request)
|
||||||
full_url = normalize_image_url(image_url, base_url)
|
full_url = normalize_image_url(image_url, base_url)
|
||||||
|
|
||||||
@@ -1142,7 +1012,6 @@ async def upload_company_logo(
|
|||||||
logger.error(f"Error uploading logo: {e}", exc_info=True)
|
logger.error(f"Error uploading logo: {e}", exc_info=True)
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.post("/company/favicon")
|
@router.post("/company/favicon")
|
||||||
async def upload_company_favicon(
|
async def upload_company_favicon(
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -1150,9 +1019,8 @@ async def upload_company_favicon(
|
|||||||
current_user: User = Depends(authorize_roles("admin")),
|
current_user: User = Depends(authorize_roles("admin")),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""Upload company favicon (Admin only)"""
|
|
||||||
try:
|
try:
|
||||||
# Validate file type (favicon can be ico, png, svg)
|
|
||||||
if not image.content_type:
|
if not image.content_type:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
@@ -1161,7 +1029,7 @@ async def upload_company_favicon(
|
|||||||
|
|
||||||
allowed_types = ['image/x-icon', 'image/vnd.microsoft.icon', 'image/png', 'image/svg+xml', 'image/ico']
|
allowed_types = ['image/x-icon', 'image/vnd.microsoft.icon', 'image/png', 'image/svg+xml', 'image/ico']
|
||||||
if image.content_type not in allowed_types:
|
if image.content_type not in allowed_types:
|
||||||
# Check filename extension as fallback
|
|
||||||
filename_lower = (image.filename or '').lower()
|
filename_lower = (image.filename or '').lower()
|
||||||
if not any(filename_lower.endswith(ext) for ext in ['.ico', '.png', '.svg']):
|
if not any(filename_lower.endswith(ext) for ext in ['.ico', '.png', '.svg']):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -1169,19 +1037,19 @@ async def upload_company_favicon(
|
|||||||
detail="Favicon must be .ico, .png, or .svg file"
|
detail="Favicon must be .ico, .png, or .svg file"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate file size (max 500KB)
|
|
||||||
content = await image.read()
|
content = await image.read()
|
||||||
if len(content) > 500 * 1024: # 500KB
|
if len(content) > 500 * 1024:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail="Favicon file size must be less than 500KB"
|
detail="Favicon file size must be less than 500KB"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create uploads directory
|
|
||||||
upload_dir = Path(__file__).parent.parent.parent / "uploads" / "company"
|
upload_dir = Path(__file__).parent.parent.parent / "uploads" / "company"
|
||||||
upload_dir.mkdir(parents=True, exist_ok=True)
|
upload_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# Delete old favicon if exists
|
|
||||||
old_favicon_setting = db.query(SystemSettings).filter(
|
old_favicon_setting = db.query(SystemSettings).filter(
|
||||||
SystemSettings.key == "company_favicon_url"
|
SystemSettings.key == "company_favicon_url"
|
||||||
).first()
|
).first()
|
||||||
@@ -1194,7 +1062,7 @@ async def upload_company_favicon(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Could not delete old favicon: {e}")
|
logger.warning(f"Could not delete old favicon: {e}")
|
||||||
|
|
||||||
# Generate filename - preserve extension but use standard name
|
|
||||||
filename_lower = (image.filename or '').lower()
|
filename_lower = (image.filename or '').lower()
|
||||||
if filename_lower.endswith('.ico'):
|
if filename_lower.endswith('.ico'):
|
||||||
filename = "favicon.ico"
|
filename = "favicon.ico"
|
||||||
@@ -1205,14 +1073,14 @@ async def upload_company_favicon(
|
|||||||
|
|
||||||
file_path = upload_dir / filename
|
file_path = upload_dir / filename
|
||||||
|
|
||||||
# Save file
|
|
||||||
async with aiofiles.open(file_path, 'wb') as f:
|
async with aiofiles.open(file_path, 'wb') as f:
|
||||||
await f.write(content)
|
await f.write(content)
|
||||||
|
|
||||||
# Store the URL in system_settings
|
|
||||||
image_url = f"/uploads/company/{filename}"
|
image_url = f"/uploads/company/{filename}"
|
||||||
|
|
||||||
# Update or create setting
|
|
||||||
favicon_setting = db.query(SystemSettings).filter(
|
favicon_setting = db.query(SystemSettings).filter(
|
||||||
SystemSettings.key == "company_favicon_url"
|
SystemSettings.key == "company_favicon_url"
|
||||||
).first()
|
).first()
|
||||||
@@ -1231,7 +1099,7 @@ async def upload_company_favicon(
|
|||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
# Return the image URL
|
|
||||||
base_url = get_base_url(request)
|
base_url = get_base_url(request)
|
||||||
full_url = normalize_image_url(image_url, base_url)
|
full_url = normalize_image_url(image_url, base_url)
|
||||||
|
|
||||||
@@ -1251,12 +1119,10 @@ async def upload_company_favicon(
|
|||||||
logger.error(f"Error uploading favicon: {e}", exc_info=True)
|
logger.error(f"Error uploading favicon: {e}", exc_info=True)
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/recaptcha")
|
@router.get("/recaptcha")
|
||||||
async def get_recaptcha_settings(
|
async def get_recaptcha_settings(
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""Get reCAPTCHA settings (Public endpoint for frontend)"""
|
|
||||||
try:
|
try:
|
||||||
site_key_setting = db.query(SystemSettings).filter(
|
site_key_setting = db.query(SystemSettings).filter(
|
||||||
SystemSettings.key == "recaptcha_site_key"
|
SystemSettings.key == "recaptcha_site_key"
|
||||||
@@ -1284,13 +1150,11 @@ async def get_recaptcha_settings(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/recaptcha/admin")
|
@router.get("/recaptcha/admin")
|
||||||
async def get_recaptcha_settings_admin(
|
async def get_recaptcha_settings_admin(
|
||||||
current_user: User = Depends(authorize_roles("admin")),
|
current_user: User = Depends(authorize_roles("admin")),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""Get reCAPTCHA settings (Admin only - includes secret key)"""
|
|
||||||
try:
|
try:
|
||||||
site_key_setting = db.query(SystemSettings).filter(
|
site_key_setting = db.query(SystemSettings).filter(
|
||||||
SystemSettings.key == "recaptcha_site_key"
|
SystemSettings.key == "recaptcha_site_key"
|
||||||
@@ -1304,7 +1168,7 @@ async def get_recaptcha_settings_admin(
|
|||||||
SystemSettings.key == "recaptcha_enabled"
|
SystemSettings.key == "recaptcha_enabled"
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
# Mask secret for security (only show last 4 characters)
|
|
||||||
def mask_key(key_value: str) -> str:
|
def mask_key(key_value: str) -> str:
|
||||||
if not key_value or len(key_value) < 4:
|
if not key_value or len(key_value) < 4:
|
||||||
return ""
|
return ""
|
||||||
@@ -1340,20 +1204,18 @@ async def get_recaptcha_settings_admin(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.put("/recaptcha")
|
@router.put("/recaptcha")
|
||||||
async def update_recaptcha_settings(
|
async def update_recaptcha_settings(
|
||||||
recaptcha_data: dict,
|
recaptcha_data: dict,
|
||||||
current_user: User = Depends(authorize_roles("admin")),
|
current_user: User = Depends(authorize_roles("admin")),
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""Update reCAPTCHA settings (Admin only)"""
|
|
||||||
try:
|
try:
|
||||||
site_key = recaptcha_data.get("recaptcha_site_key", "").strip()
|
site_key = recaptcha_data.get("recaptcha_site_key", "").strip()
|
||||||
secret_key = recaptcha_data.get("recaptcha_secret_key", "").strip()
|
secret_key = recaptcha_data.get("recaptcha_secret_key", "").strip()
|
||||||
enabled = recaptcha_data.get("recaptcha_enabled", False)
|
enabled = recaptcha_data.get("recaptcha_enabled", False)
|
||||||
|
|
||||||
# Update or create site key setting
|
|
||||||
if site_key:
|
if site_key:
|
||||||
setting = db.query(SystemSettings).filter(
|
setting = db.query(SystemSettings).filter(
|
||||||
SystemSettings.key == "recaptcha_site_key"
|
SystemSettings.key == "recaptcha_site_key"
|
||||||
@@ -1371,7 +1233,7 @@ async def update_recaptcha_settings(
|
|||||||
)
|
)
|
||||||
db.add(setting)
|
db.add(setting)
|
||||||
|
|
||||||
# Update or create secret key setting
|
|
||||||
if secret_key:
|
if secret_key:
|
||||||
setting = db.query(SystemSettings).filter(
|
setting = db.query(SystemSettings).filter(
|
||||||
SystemSettings.key == "recaptcha_secret_key"
|
SystemSettings.key == "recaptcha_secret_key"
|
||||||
@@ -1389,7 +1251,7 @@ async def update_recaptcha_settings(
|
|||||||
)
|
)
|
||||||
db.add(setting)
|
db.add(setting)
|
||||||
|
|
||||||
# Update or create enabled setting
|
|
||||||
setting = db.query(SystemSettings).filter(
|
setting = db.query(SystemSettings).filter(
|
||||||
SystemSettings.key == "recaptcha_enabled"
|
SystemSettings.key == "recaptcha_enabled"
|
||||||
).first()
|
).first()
|
||||||
@@ -1408,7 +1270,7 @@ async def update_recaptcha_settings(
|
|||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
# Return masked values
|
|
||||||
def mask_key(key_value: str) -> str:
|
def mask_key(key_value: str) -> str:
|
||||||
if not key_value or len(key_value) < 4:
|
if not key_value or len(key_value) < 4:
|
||||||
return ""
|
return ""
|
||||||
@@ -1432,13 +1294,11 @@ async def update_recaptcha_settings(
|
|||||||
db.rollback()
|
db.rollback()
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.post("/recaptcha/verify")
|
@router.post("/recaptcha/verify")
|
||||||
async def verify_recaptcha(
|
async def verify_recaptcha(
|
||||||
verification_data: dict,
|
verification_data: dict,
|
||||||
db: Session = Depends(get_db)
|
db: Session = Depends(get_db)
|
||||||
):
|
):
|
||||||
"""Verify reCAPTCHA token (Public endpoint)"""
|
|
||||||
try:
|
try:
|
||||||
token = verification_data.get("token", "").strip()
|
token = verification_data.get("token", "").strip()
|
||||||
|
|
||||||
@@ -1448,7 +1308,7 @@ async def verify_recaptcha(
|
|||||||
detail="reCAPTCHA token is required"
|
detail="reCAPTCHA token is required"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get reCAPTCHA settings
|
|
||||||
enabled_setting = db.query(SystemSettings).filter(
|
enabled_setting = db.query(SystemSettings).filter(
|
||||||
SystemSettings.key == "recaptcha_enabled"
|
SystemSettings.key == "recaptcha_enabled"
|
||||||
).first()
|
).first()
|
||||||
@@ -1457,13 +1317,13 @@ async def verify_recaptcha(
|
|||||||
SystemSettings.key == "recaptcha_secret_key"
|
SystemSettings.key == "recaptcha_secret_key"
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
# Check if reCAPTCHA is enabled
|
|
||||||
is_enabled = False
|
is_enabled = False
|
||||||
if enabled_setting:
|
if enabled_setting:
|
||||||
is_enabled = enabled_setting.value.lower() == "true" if enabled_setting.value else False
|
is_enabled = enabled_setting.value.lower() == "true" if enabled_setting.value else False
|
||||||
|
|
||||||
if not is_enabled:
|
if not is_enabled:
|
||||||
# If disabled, always return success
|
|
||||||
return {
|
return {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"data": {
|
"data": {
|
||||||
@@ -1478,7 +1338,7 @@ async def verify_recaptcha(
|
|||||||
detail="reCAPTCHA secret key is not configured"
|
detail="reCAPTCHA secret key is not configured"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify with Google reCAPTCHA API
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
@@ -1498,8 +1358,8 @@ async def verify_recaptcha(
|
|||||||
"status": "success",
|
"status": "success",
|
||||||
"data": {
|
"data": {
|
||||||
"verified": True,
|
"verified": True,
|
||||||
"score": result.get("score"), # For v3
|
"score": result.get("score"),
|
||||||
"action": result.get("action") # For v3
|
"action": result.get("action")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -3,323 +3,136 @@ from sqlalchemy.orm import Session
|
|||||||
from sqlalchemy import or_
|
from sqlalchemy import or_
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import bcrypt
|
import bcrypt
|
||||||
|
|
||||||
from ..config.database import get_db
|
from ..config.database import get_db
|
||||||
from ..middleware.auth import get_current_user, authorize_roles
|
from ..middleware.auth import get_current_user, authorize_roles
|
||||||
from ..models.user import User
|
from ..models.user import User
|
||||||
from ..models.role import Role
|
from ..models.role import Role
|
||||||
from ..models.booking import Booking, BookingStatus
|
from ..models.booking import Booking, BookingStatus
|
||||||
|
router = APIRouter(prefix='/users', tags=['users'])
|
||||||
|
|
||||||
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)):
|
||||||
|
|
||||||
@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:
|
try:
|
||||||
query = db.query(User)
|
query = db.query(User)
|
||||||
|
|
||||||
# Filter by search (full_name, email, phone)
|
|
||||||
if search:
|
if search:
|
||||||
query = query.filter(
|
query = query.filter(or_(User.full_name.like(f'%{search}%'), User.email.like(f'%{search}%'), User.phone.like(f'%{search}%')))
|
||||||
or_(
|
|
||||||
User.full_name.like(f"%{search}%"),
|
|
||||||
User.email.like(f"%{search}%"),
|
|
||||||
User.phone.like(f"%{search}%")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Filter by role
|
|
||||||
if role:
|
if role:
|
||||||
role_map = {"admin": 1, "staff": 2, "customer": 3}
|
role_map = {'admin': 1, 'staff': 2, 'customer': 3}
|
||||||
if role in role_map:
|
if role in role_map:
|
||||||
query = query.filter(User.role_id == role_map[role])
|
query = query.filter(User.role_id == role_map[role])
|
||||||
|
|
||||||
# Filter by status
|
|
||||||
if status_filter:
|
if status_filter:
|
||||||
is_active = status_filter == "active"
|
is_active = status_filter == 'active'
|
||||||
query = query.filter(User.is_active == is_active)
|
query = query.filter(User.is_active == is_active)
|
||||||
|
|
||||||
# Get total count
|
|
||||||
total = query.count()
|
total = query.count()
|
||||||
|
|
||||||
# Apply pagination
|
|
||||||
offset = (page - 1) * limit
|
offset = (page - 1) * limit
|
||||||
users = query.order_by(User.created_at.desc()).offset(offset).limit(limit).all()
|
users = query.order_by(User.created_at.desc()).offset(offset).limit(limit).all()
|
||||||
|
|
||||||
# Transform users
|
|
||||||
result = []
|
result = []
|
||||||
for user in users:
|
for user in users:
|
||||||
user_dict = {
|
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, 'currency': getattr(user, 'currency', 'VND'), '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}
|
||||||
"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,
|
|
||||||
"currency": getattr(user, 'currency', 'VND'),
|
|
||||||
"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)
|
result.append(user_dict)
|
||||||
|
return {'status': 'success', 'data': {'users': result, 'pagination': {'total': total, 'page': page, 'limit': limit, 'totalPages': (total + limit - 1) // limit}}}
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"data": {
|
|
||||||
"users": result,
|
|
||||||
"pagination": {
|
|
||||||
"total": total,
|
|
||||||
"page": page,
|
|
||||||
"limit": limit,
|
|
||||||
"totalPages": (total + limit - 1) // limit,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.get('/{id}', dependencies=[Depends(authorize_roles('admin'))])
|
||||||
@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)):
|
||||||
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:
|
try:
|
||||||
user = db.query(User).filter(User.id == id).first()
|
user = db.query(User).filter(User.id == id).first()
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
raise HTTPException(status_code=404, detail='User not found')
|
||||||
|
bookings = db.query(Booking).filter(Booking.user_id == id).order_by(Booking.created_at.desc()).limit(5).all()
|
||||||
# Get recent bookings
|
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, 'currency': getattr(user, 'currency', 'VND'), '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]}
|
||||||
bookings = db.query(Booking).filter(
|
return {'status': 'success', 'data': {'user': user_dict}}
|
||||||
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,
|
|
||||||
"currency": getattr(user, 'currency', 'VND'),
|
|
||||||
"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:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.post('/', dependencies=[Depends(authorize_roles('admin'))])
|
||||||
@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)):
|
||||||
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:
|
try:
|
||||||
email = user_data.get("email")
|
email = user_data.get('email')
|
||||||
password = user_data.get("password")
|
password = user_data.get('password')
|
||||||
full_name = user_data.get("full_name")
|
full_name = user_data.get('full_name')
|
||||||
phone_number = user_data.get("phone_number")
|
phone_number = user_data.get('phone_number')
|
||||||
role = user_data.get("role", "customer")
|
role = user_data.get('role', 'customer')
|
||||||
status = user_data.get("status", "active")
|
status = user_data.get('status', 'active')
|
||||||
|
role_map = {'admin': 1, 'staff': 2, 'customer': 3}
|
||||||
# Map role string to role_id
|
|
||||||
role_map = {"admin": 1, "staff": 2, "customer": 3}
|
|
||||||
role_id = role_map.get(role, 3)
|
role_id = role_map.get(role, 3)
|
||||||
|
|
||||||
# Check if email exists
|
|
||||||
existing = db.query(User).filter(User.email == email).first()
|
existing = db.query(User).filter(User.email == email).first()
|
||||||
if existing:
|
if existing:
|
||||||
raise HTTPException(status_code=400, detail="Email already exists")
|
raise HTTPException(status_code=400, detail='Email already exists')
|
||||||
|
|
||||||
# Hash password
|
|
||||||
password_bytes = password.encode('utf-8')
|
password_bytes = password.encode('utf-8')
|
||||||
salt = bcrypt.gensalt()
|
salt = bcrypt.gensalt()
|
||||||
hashed_password = bcrypt.hashpw(password_bytes, salt).decode('utf-8')
|
hashed_password = bcrypt.hashpw(password_bytes, salt).decode('utf-8')
|
||||||
|
user = User(email=email, password=hashed_password, full_name=full_name, phone=phone_number, role_id=role_id, is_active=status == 'active')
|
||||||
# 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.add(user)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(user)
|
db.refresh(user)
|
||||||
|
user_dict = {'id': user.id, 'email': user.email, 'full_name': user.full_name, 'phone': user.phone, 'phone_number': user.phone, 'currency': getattr(user, 'currency', 'VND'), 'role_id': user.role_id, 'is_active': user.is_active}
|
||||||
# Remove password from response
|
return {'status': 'success', 'message': 'User created successfully', 'data': {'user': user_dict}}
|
||||||
user_dict = {
|
|
||||||
"id": user.id,
|
|
||||||
"email": user.email,
|
|
||||||
"full_name": user.full_name,
|
|
||||||
"phone": user.phone,
|
|
||||||
"phone_number": user.phone,
|
|
||||||
"currency": getattr(user, 'currency', 'VND'),
|
|
||||||
"role_id": user.role_id,
|
|
||||||
"is_active": user.is_active,
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"message": "User created successfully",
|
|
||||||
"data": {"user": user_dict}
|
|
||||||
}
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.put('/{id}')
|
||||||
@router.put("/{id}")
|
async def update_user(id: int, user_data: dict, current_user: User=Depends(get_current_user), db: Session=Depends(get_db)):
|
||||||
async def update_user(
|
|
||||||
id: int,
|
|
||||||
user_data: dict,
|
|
||||||
current_user: User = Depends(get_current_user),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Update user"""
|
|
||||||
try:
|
try:
|
||||||
# Users can only update themselves unless they're admin
|
|
||||||
if current_user.role_id != 1 and current_user.id != id:
|
if current_user.role_id != 1 and current_user.id != id:
|
||||||
raise HTTPException(status_code=403, detail="Forbidden")
|
raise HTTPException(status_code=403, detail='Forbidden')
|
||||||
|
|
||||||
user = db.query(User).filter(User.id == id).first()
|
user = db.query(User).filter(User.id == id).first()
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
raise HTTPException(status_code=404, detail='User not found')
|
||||||
|
email = user_data.get('email')
|
||||||
# Check if email is being changed and if it's taken
|
|
||||||
email = user_data.get("email")
|
|
||||||
if email and email != user.email:
|
if email and email != user.email:
|
||||||
existing = db.query(User).filter(User.email == email).first()
|
existing = db.query(User).filter(User.email == email).first()
|
||||||
if existing:
|
if existing:
|
||||||
raise HTTPException(status_code=400, detail="Email already exists")
|
raise HTTPException(status_code=400, detail='Email already exists')
|
||||||
|
role_map = {'admin': 1, 'staff': 2, 'customer': 3}
|
||||||
# Map role string to role_id (only admin can change role)
|
if 'full_name' in user_data:
|
||||||
role_map = {"admin": 1, "staff": 2, "customer": 3}
|
user.full_name = user_data['full_name']
|
||||||
|
if 'email' in user_data and current_user.role_id == 1:
|
||||||
# Update fields
|
user.email = user_data['email']
|
||||||
if "full_name" in user_data:
|
if 'phone_number' in user_data:
|
||||||
user.full_name = user_data["full_name"]
|
user.phone = user_data['phone_number']
|
||||||
if "email" in user_data and current_user.role_id == 1:
|
if 'role' in user_data and current_user.role_id == 1:
|
||||||
user.email = user_data["email"]
|
user.role_id = role_map.get(user_data['role'], 3)
|
||||||
if "phone_number" in user_data:
|
if 'status' in user_data and current_user.role_id == 1:
|
||||||
user.phone = user_data["phone_number"]
|
user.is_active = user_data['status'] == 'active'
|
||||||
if "role" in user_data and current_user.role_id == 1:
|
if 'currency' in user_data:
|
||||||
user.role_id = role_map.get(user_data["role"], 3)
|
currency = user_data['currency']
|
||||||
if "status" in user_data and current_user.role_id == 1:
|
|
||||||
user.is_active = user_data["status"] == "active"
|
|
||||||
if "currency" in user_data:
|
|
||||||
currency = user_data["currency"]
|
|
||||||
if len(currency) == 3 and currency.isalpha():
|
if len(currency) == 3 and currency.isalpha():
|
||||||
user.currency = currency.upper()
|
user.currency = currency.upper()
|
||||||
if "password" in user_data:
|
if 'password' in user_data:
|
||||||
password_bytes = user_data["password"].encode('utf-8')
|
password_bytes = user_data['password'].encode('utf-8')
|
||||||
salt = bcrypt.gensalt()
|
salt = bcrypt.gensalt()
|
||||||
user.password = bcrypt.hashpw(password_bytes, salt).decode('utf-8')
|
user.password = bcrypt.hashpw(password_bytes, salt).decode('utf-8')
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(user)
|
db.refresh(user)
|
||||||
|
user_dict = {'id': user.id, 'email': user.email, 'full_name': user.full_name, 'phone': user.phone, 'phone_number': user.phone, 'currency': getattr(user, 'currency', 'VND'), 'role_id': user.role_id, 'is_active': user.is_active}
|
||||||
# Remove password from response
|
return {'status': 'success', 'message': 'User updated successfully', 'data': {'user': user_dict}}
|
||||||
user_dict = {
|
|
||||||
"id": user.id,
|
|
||||||
"email": user.email,
|
|
||||||
"full_name": user.full_name,
|
|
||||||
"phone": user.phone,
|
|
||||||
"phone_number": user.phone,
|
|
||||||
"currency": getattr(user, 'currency', 'VND'),
|
|
||||||
"role_id": user.role_id,
|
|
||||||
"is_active": user.is_active,
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"message": "User updated successfully",
|
|
||||||
"data": {"user": user_dict}
|
|
||||||
}
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@router.delete('/{id}', dependencies=[Depends(authorize_roles('admin'))])
|
||||||
@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)):
|
||||||
async def delete_user(
|
|
||||||
id: int,
|
|
||||||
current_user: User = Depends(authorize_roles("admin")),
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""Delete user (Admin only)"""
|
|
||||||
try:
|
try:
|
||||||
user = db.query(User).filter(User.id == id).first()
|
user = db.query(User).filter(User.id == id).first()
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
raise HTTPException(status_code=404, detail='User not found')
|
||||||
|
active_bookings = db.query(Booking).filter(Booking.user_id == id, Booking.status.in_([BookingStatus.pending, BookingStatus.confirmed, BookingStatus.checked_in])).count()
|
||||||
# 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:
|
if active_bookings > 0:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=400, detail='Cannot delete user with active bookings')
|
||||||
status_code=400,
|
|
||||||
detail="Cannot delete user with active bookings"
|
|
||||||
)
|
|
||||||
|
|
||||||
db.delete(user)
|
db.delete(user)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
return {'status': 'success', 'message': 'User deleted successfully'}
|
||||||
return {
|
|
||||||
"status": "success",
|
|
||||||
"message": "User deleted successfully"
|
|
||||||
}
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
@@ -1,68 +1,32 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
class CookiePolicySettings(BaseModel):
|
class CookiePolicySettings(BaseModel):
|
||||||
"""
|
analytics_enabled: bool = Field(default=True, description='If false, analytics cookies/scripts should not be used at all.')
|
||||||
Admin-configurable global cookie policy.
|
marketing_enabled: bool = Field(default=True, description='If false, marketing cookies/scripts should not be used at all.')
|
||||||
Controls which categories can be used in the application.
|
preferences_enabled: bool = Field(default=True, description='If false, preference cookies should not be used at all.')
|
||||||
"""
|
|
||||||
|
|
||||||
analytics_enabled: bool = Field(
|
|
||||||
default=True,
|
|
||||||
description="If false, analytics cookies/scripts should not be used at all.",
|
|
||||||
)
|
|
||||||
marketing_enabled: bool = Field(
|
|
||||||
default=True,
|
|
||||||
description="If false, marketing cookies/scripts should not be used at all.",
|
|
||||||
)
|
|
||||||
preferences_enabled: bool = Field(
|
|
||||||
default=True,
|
|
||||||
description="If false, preference cookies should not be used at all.",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CookiePolicySettingsResponse(BaseModel):
|
class CookiePolicySettingsResponse(BaseModel):
|
||||||
status: str = Field(default="success")
|
status: str = Field(default='success')
|
||||||
data: CookiePolicySettings
|
data: CookiePolicySettings
|
||||||
updated_at: Optional[datetime] = None
|
updated_at: Optional[datetime] = None
|
||||||
updated_by: Optional[str] = None
|
updated_by: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class CookieIntegrationSettings(BaseModel):
|
class CookieIntegrationSettings(BaseModel):
|
||||||
"""
|
ga_measurement_id: Optional[str] = Field(default=None, description='Google Analytics 4 measurement ID (e.g. G-XXXXXXX).')
|
||||||
IDs for well-known third-party integrations, configured by admin.
|
fb_pixel_id: Optional[str] = Field(default=None, description='Meta (Facebook) Pixel ID.')
|
||||||
"""
|
|
||||||
|
|
||||||
ga_measurement_id: Optional[str] = Field(
|
|
||||||
default=None, description="Google Analytics 4 measurement ID (e.g. G-XXXXXXX)."
|
|
||||||
)
|
|
||||||
fb_pixel_id: Optional[str] = Field(
|
|
||||||
default=None, description="Meta (Facebook) Pixel ID."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CookieIntegrationSettingsResponse(BaseModel):
|
class CookieIntegrationSettingsResponse(BaseModel):
|
||||||
status: str = Field(default="success")
|
status: str = Field(default='success')
|
||||||
data: CookieIntegrationSettings
|
data: CookieIntegrationSettings
|
||||||
updated_at: Optional[datetime] = None
|
updated_at: Optional[datetime] = None
|
||||||
updated_by: Optional[str] = None
|
updated_by: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class PublicPrivacyConfig(BaseModel):
|
class PublicPrivacyConfig(BaseModel):
|
||||||
"""
|
|
||||||
Publicly consumable privacy configuration for the frontend.
|
|
||||||
Does not expose any secrets, only IDs and flags.
|
|
||||||
"""
|
|
||||||
|
|
||||||
policy: CookiePolicySettings
|
policy: CookiePolicySettings
|
||||||
integrations: CookieIntegrationSettings
|
integrations: CookieIntegrationSettings
|
||||||
|
|
||||||
|
|
||||||
class PublicPrivacyConfigResponse(BaseModel):
|
class PublicPrivacyConfigResponse(BaseModel):
|
||||||
status: str = Field(default="success")
|
status: str = Field(default='success')
|
||||||
data: PublicPrivacyConfig
|
data: PublicPrivacyConfig
|
||||||
|
|
||||||
|
|
||||||
@@ -1,64 +1,58 @@
|
|||||||
from pydantic import BaseModel, EmailStr, Field, validator
|
from pydantic import BaseModel, EmailStr, Field, validator
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
class RegisterRequest(BaseModel):
|
class RegisterRequest(BaseModel):
|
||||||
name: str = Field(..., min_length=2, max_length=50)
|
name: str = Field(..., min_length=2, max_length=50)
|
||||||
email: EmailStr
|
email: EmailStr
|
||||||
password: str = Field(..., min_length=8)
|
password: str = Field(..., min_length=8)
|
||||||
phone: Optional[str] = None
|
phone: Optional[str] = None
|
||||||
|
|
||||||
@validator("password")
|
@validator('password')
|
||||||
def validate_password(cls, v):
|
def validate_password(cls, v):
|
||||||
if len(v) < 8:
|
if len(v) < 8:
|
||||||
raise ValueError("Password must be at least 8 characters")
|
raise ValueError('Password must be at least 8 characters')
|
||||||
if not any(c.isupper() for c in v):
|
if not any((c.isupper() for c in v)):
|
||||||
raise ValueError("Password must contain at least one uppercase letter")
|
raise ValueError('Password must contain at least one uppercase letter')
|
||||||
if not any(c.islower() for c in v):
|
if not any((c.islower() for c in v)):
|
||||||
raise ValueError("Password must contain at least one lowercase letter")
|
raise ValueError('Password must contain at least one lowercase letter')
|
||||||
if not any(c.isdigit() for c in v):
|
if not any((c.isdigit() for c in v)):
|
||||||
raise ValueError("Password must contain at least one number")
|
raise ValueError('Password must contain at least one number')
|
||||||
return v
|
return v
|
||||||
|
|
||||||
@validator("phone")
|
@validator('phone')
|
||||||
def validate_phone(cls, v):
|
def validate_phone(cls, v):
|
||||||
if v and not v.isdigit() or (v and len(v) not in [10, 11]):
|
if v and (not v.isdigit()) or (v and len(v) not in [10, 11]):
|
||||||
raise ValueError("Phone must be 10-11 digits")
|
raise ValueError('Phone must be 10-11 digits')
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
class LoginRequest(BaseModel):
|
class LoginRequest(BaseModel):
|
||||||
email: EmailStr
|
email: EmailStr
|
||||||
password: str
|
password: str
|
||||||
rememberMe: Optional[bool] = False
|
rememberMe: Optional[bool] = False
|
||||||
mfaToken: Optional[str] = None
|
mfaToken: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class RefreshTokenRequest(BaseModel):
|
class RefreshTokenRequest(BaseModel):
|
||||||
refreshToken: Optional[str] = None
|
refreshToken: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class ForgotPasswordRequest(BaseModel):
|
class ForgotPasswordRequest(BaseModel):
|
||||||
email: EmailStr
|
email: EmailStr
|
||||||
|
|
||||||
|
|
||||||
class ResetPasswordRequest(BaseModel):
|
class ResetPasswordRequest(BaseModel):
|
||||||
token: str
|
token: str
|
||||||
password: str = Field(..., min_length=8)
|
password: str = Field(..., min_length=8)
|
||||||
|
|
||||||
@validator("password")
|
@validator('password')
|
||||||
def validate_password(cls, v):
|
def validate_password(cls, v):
|
||||||
if len(v) < 8:
|
if len(v) < 8:
|
||||||
raise ValueError("Password must be at least 8 characters")
|
raise ValueError('Password must be at least 8 characters')
|
||||||
if not any(c.isupper() for c in v):
|
if not any((c.isupper() for c in v)):
|
||||||
raise ValueError("Password must contain at least one uppercase letter")
|
raise ValueError('Password must contain at least one uppercase letter')
|
||||||
if not any(c.islower() for c in v):
|
if not any((c.islower() for c in v)):
|
||||||
raise ValueError("Password must contain at least one lowercase letter")
|
raise ValueError('Password must contain at least one lowercase letter')
|
||||||
if not any(c.isdigit() for c in v):
|
if not any((c.isdigit() for c in v)):
|
||||||
raise ValueError("Password must contain at least one number")
|
raise ValueError('Password must contain at least one number')
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
class UserResponse(BaseModel):
|
class UserResponse(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
name: str
|
name: str
|
||||||
@@ -71,38 +65,30 @@ class UserResponse(BaseModel):
|
|||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
class AuthResponse(BaseModel):
|
class AuthResponse(BaseModel):
|
||||||
user: UserResponse
|
user: UserResponse
|
||||||
token: str
|
token: str
|
||||||
refreshToken: Optional[str] = None
|
refreshToken: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class TokenResponse(BaseModel):
|
class TokenResponse(BaseModel):
|
||||||
token: str
|
token: str
|
||||||
|
|
||||||
|
|
||||||
class MessageResponse(BaseModel):
|
class MessageResponse(BaseModel):
|
||||||
status: str
|
status: str
|
||||||
message: str
|
message: str
|
||||||
|
|
||||||
|
|
||||||
class MFAInitResponse(BaseModel):
|
class MFAInitResponse(BaseModel):
|
||||||
secret: str
|
secret: str
|
||||||
qr_code: str # Base64 data URL
|
qr_code: str
|
||||||
|
|
||||||
|
|
||||||
class EnableMFARequest(BaseModel):
|
class EnableMFARequest(BaseModel):
|
||||||
secret: str
|
secret: str
|
||||||
verification_token: str
|
verification_token: str
|
||||||
|
|
||||||
|
|
||||||
class VerifyMFARequest(BaseModel):
|
class VerifyMFARequest(BaseModel):
|
||||||
token: str
|
token: str
|
||||||
is_backup_code: Optional[bool] = False
|
is_backup_code: Optional[bool] = False
|
||||||
|
|
||||||
|
|
||||||
class MFAStatusResponse(BaseModel):
|
class MFAStatusResponse(BaseModel):
|
||||||
mfa_enabled: bool
|
mfa_enabled: bool
|
||||||
backup_codes_count: int
|
backup_codes_count: int
|
||||||
|
|
||||||
@@ -1,70 +1,24 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
class CookieCategoryPreferences(BaseModel):
|
class CookieCategoryPreferences(BaseModel):
|
||||||
"""
|
necessary: bool = Field(default=True, description='Strictly necessary cookies (always enabled as they are required for core functionality).')
|
||||||
Granular consent for different cookie categories.
|
analytics: bool = Field(default=False, description='Allow anonymous analytics and performance cookies.')
|
||||||
|
marketing: bool = Field(default=False, description='Allow marketing and advertising cookies.')
|
||||||
- necessary: required for the site to function (always true, not revocable)
|
preferences: bool = Field(default=False, description='Allow preference cookies (e.g. language, layout settings).')
|
||||||
- analytics: usage analytics, performance tracking
|
|
||||||
- marketing: advertising, remarketing cookies
|
|
||||||
- preferences: UI / language / personalization preferences
|
|
||||||
"""
|
|
||||||
|
|
||||||
necessary: bool = Field(
|
|
||||||
default=True,
|
|
||||||
description="Strictly necessary cookies (always enabled as they are required for core functionality).",
|
|
||||||
)
|
|
||||||
analytics: bool = Field(
|
|
||||||
default=False, description="Allow anonymous analytics and performance cookies."
|
|
||||||
)
|
|
||||||
marketing: bool = Field(
|
|
||||||
default=False, description="Allow marketing and advertising cookies."
|
|
||||||
)
|
|
||||||
preferences: bool = Field(
|
|
||||||
default=False,
|
|
||||||
description="Allow preference cookies (e.g. language, layout settings).",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CookieConsent(BaseModel):
|
class CookieConsent(BaseModel):
|
||||||
"""
|
version: int = Field(default=1, description='Consent schema version for future migrations.')
|
||||||
Persisted cookie consent state.
|
updated_at: datetime = Field(default_factory=datetime.utcnow, description='Last time consent was updated.')
|
||||||
Stored in an HttpOnly cookie and exposed via the API.
|
has_decided: bool = Field(default=False, description='Whether the user has actively made a consent choice.')
|
||||||
"""
|
categories: CookieCategoryPreferences = Field(default_factory=CookieCategoryPreferences, description='Granular per-category consent.')
|
||||||
|
|
||||||
version: int = Field(
|
|
||||||
default=1, description="Consent schema version for future migrations."
|
|
||||||
)
|
|
||||||
updated_at: datetime = Field(
|
|
||||||
default_factory=datetime.utcnow, description="Last time consent was updated."
|
|
||||||
)
|
|
||||||
has_decided: bool = Field(
|
|
||||||
default=False,
|
|
||||||
description="Whether the user has actively made a consent choice.",
|
|
||||||
)
|
|
||||||
categories: CookieCategoryPreferences = Field(
|
|
||||||
default_factory=CookieCategoryPreferences,
|
|
||||||
description="Granular per-category consent.",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CookieConsentResponse(BaseModel):
|
class CookieConsentResponse(BaseModel):
|
||||||
status: str = Field(default="success")
|
status: str = Field(default='success')
|
||||||
data: CookieConsent
|
data: CookieConsent
|
||||||
|
|
||||||
|
|
||||||
class UpdateCookieConsentRequest(BaseModel):
|
class UpdateCookieConsentRequest(BaseModel):
|
||||||
"""
|
|
||||||
Request body for updating cookie consent.
|
|
||||||
'necessary' is ignored on write and always treated as True by the server.
|
|
||||||
"""
|
|
||||||
|
|
||||||
analytics: Optional[bool] = None
|
analytics: Optional[bool] = None
|
||||||
marketing: Optional[bool] = None
|
marketing: Optional[bool] = None
|
||||||
preferences: Optional[bool] = None
|
preferences: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
@@ -1,82 +1,20 @@
|
|||||||
"""
|
|
||||||
Audit logging service for tracking important actions
|
|
||||||
"""
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from typing import Optional, Dict, Any
|
from typing import Optional, Dict, Any
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from ..models.audit_log import AuditLog
|
from ..models.audit_log import AuditLog
|
||||||
from ..config.logging_config import get_logger
|
from ..config.logging_config import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class AuditService:
|
class AuditService:
|
||||||
"""Service for creating audit log entries"""
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def log_action(
|
async def log_action(db: Session, action: str, resource_type: str, user_id: Optional[int]=None, resource_id: Optional[int]=None, ip_address: Optional[str]=None, user_agent: Optional[str]=None, request_id: Optional[str]=None, details: Optional[Dict[str, Any]]=None, status: str='success', error_message: Optional[str]=None):
|
||||||
db: Session,
|
|
||||||
action: str,
|
|
||||||
resource_type: str,
|
|
||||||
user_id: Optional[int] = None,
|
|
||||||
resource_id: Optional[int] = None,
|
|
||||||
ip_address: Optional[str] = None,
|
|
||||||
user_agent: Optional[str] = None,
|
|
||||||
request_id: Optional[str] = None,
|
|
||||||
details: Optional[Dict[str, Any]] = None,
|
|
||||||
status: str = "success",
|
|
||||||
error_message: Optional[str] = None
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Create an audit log entry
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: Database session
|
|
||||||
action: Action performed (e.g., "user.created", "booking.cancelled")
|
|
||||||
resource_type: Type of resource (e.g., "user", "booking")
|
|
||||||
user_id: ID of user who performed the action
|
|
||||||
resource_id: ID of the resource affected
|
|
||||||
ip_address: IP address of the request
|
|
||||||
user_agent: User agent string
|
|
||||||
request_id: Request ID for tracing
|
|
||||||
details: Additional context as dictionary
|
|
||||||
status: Status of the action (success, failed, error)
|
|
||||||
error_message: Error message if action failed
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
audit_log = AuditLog(
|
audit_log = AuditLog(user_id=user_id, action=action, resource_type=resource_type, resource_id=resource_id, ip_address=ip_address, user_agent=user_agent, request_id=request_id, details=details, status=status, error_message=error_message)
|
||||||
user_id=user_id,
|
|
||||||
action=action,
|
|
||||||
resource_type=resource_type,
|
|
||||||
resource_id=resource_id,
|
|
||||||
ip_address=ip_address,
|
|
||||||
user_agent=user_agent,
|
|
||||||
request_id=request_id,
|
|
||||||
details=details,
|
|
||||||
status=status,
|
|
||||||
error_message=error_message
|
|
||||||
)
|
|
||||||
|
|
||||||
db.add(audit_log)
|
db.add(audit_log)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
logger.info(f'Audit log created: {action} on {resource_type}', extra={'action': action, 'resource_type': resource_type, 'resource_id': resource_id, 'user_id': user_id, 'status': status, 'request_id': request_id})
|
||||||
logger.info(
|
|
||||||
f"Audit log created: {action} on {resource_type}",
|
|
||||||
extra={
|
|
||||||
"action": action,
|
|
||||||
"resource_type": resource_type,
|
|
||||||
"resource_id": resource_id,
|
|
||||||
"user_id": user_id,
|
|
||||||
"status": status,
|
|
||||||
"request_id": request_id
|
|
||||||
}
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to create audit log: {str(e)}", exc_info=True)
|
logger.error(f'Failed to create audit log: {str(e)}', exc_info=True)
|
||||||
db.rollback()
|
db.rollback()
|
||||||
# Don't raise exception - audit logging failures shouldn't break the app
|
audit_service = AuditService()
|
||||||
|
|
||||||
|
|
||||||
# Global audit service instance
|
|
||||||
audit_service = AuditService()
|
|
||||||
|
|
||||||
@@ -22,17 +22,15 @@ import os
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class AuthService:
|
class AuthService:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# Use settings, fallback to env vars, then to defaults for development
|
|
||||||
self.jwt_secret = getattr(settings, 'JWT_SECRET', None) or os.getenv("JWT_SECRET", "dev-secret-key-change-in-production-12345")
|
self.jwt_secret = getattr(settings, 'JWT_SECRET', None) or os.getenv("JWT_SECRET", "dev-secret-key-change-in-production-12345")
|
||||||
self.jwt_refresh_secret = os.getenv("JWT_REFRESH_SECRET") or (self.jwt_secret + "-refresh")
|
self.jwt_refresh_secret = os.getenv("JWT_REFRESH_SECRET") or (self.jwt_secret + "-refresh")
|
||||||
self.jwt_expires_in = os.getenv("JWT_EXPIRES_IN", "1h")
|
self.jwt_expires_in = os.getenv("JWT_EXPIRES_IN", "1h")
|
||||||
self.jwt_refresh_expires_in = os.getenv("JWT_REFRESH_EXPIRES_IN", "7d")
|
self.jwt_refresh_expires_in = os.getenv("JWT_REFRESH_EXPIRES_IN", "7d")
|
||||||
|
|
||||||
def generate_tokens(self, user_id: int) -> dict:
|
def generate_tokens(self, user_id: int) -> dict:
|
||||||
"""Generate JWT tokens"""
|
|
||||||
access_token = jwt.encode(
|
access_token = jwt.encode(
|
||||||
{"userId": user_id},
|
{"userId": user_id},
|
||||||
self.jwt_secret,
|
self.jwt_secret,
|
||||||
@@ -48,24 +46,20 @@ class AuthService:
|
|||||||
return {"accessToken": access_token, "refreshToken": refresh_token}
|
return {"accessToken": access_token, "refreshToken": refresh_token}
|
||||||
|
|
||||||
def verify_access_token(self, token: str) -> dict:
|
def verify_access_token(self, token: str) -> dict:
|
||||||
"""Verify JWT access token"""
|
|
||||||
return jwt.decode(token, self.jwt_secret, algorithms=["HS256"])
|
return jwt.decode(token, self.jwt_secret, algorithms=["HS256"])
|
||||||
|
|
||||||
def verify_refresh_token(self, token: str) -> dict:
|
def verify_refresh_token(self, token: str) -> dict:
|
||||||
"""Verify JWT refresh token"""
|
|
||||||
return jwt.decode(token, self.jwt_refresh_secret, algorithms=["HS256"])
|
return jwt.decode(token, self.jwt_refresh_secret, algorithms=["HS256"])
|
||||||
|
|
||||||
def hash_password(self, password: str) -> str:
|
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')
|
password_bytes = password.encode('utf-8')
|
||||||
# Generate salt and hash password
|
|
||||||
salt = bcrypt.gensalt()
|
salt = bcrypt.gensalt()
|
||||||
hashed = bcrypt.hashpw(password_bytes, salt)
|
hashed = bcrypt.hashpw(password_bytes, salt)
|
||||||
return hashed.decode('utf-8')
|
return hashed.decode('utf-8')
|
||||||
|
|
||||||
def verify_password(self, plain_password: str, hashed_password: str) -> bool:
|
def verify_password(self, plain_password: str, hashed_password: str) -> bool:
|
||||||
"""Verify password using bcrypt"""
|
|
||||||
try:
|
try:
|
||||||
password_bytes = plain_password.encode('utf-8')
|
password_bytes = plain_password.encode('utf-8')
|
||||||
hashed_bytes = hashed_password.encode('utf-8')
|
hashed_bytes = hashed_password.encode('utf-8')
|
||||||
@@ -74,7 +68,6 @@ class AuthService:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def format_user_response(self, user: User) -> dict:
|
def format_user_response(self, user: User) -> dict:
|
||||||
"""Format user response"""
|
|
||||||
return {
|
return {
|
||||||
"id": user.id,
|
"id": user.id,
|
||||||
"name": user.full_name,
|
"name": user.full_name,
|
||||||
@@ -88,34 +81,28 @@ class AuthService:
|
|||||||
}
|
}
|
||||||
|
|
||||||
async def register(self, db: Session, name: str, email: str, password: str, phone: Optional[str] = None) -> dict:
|
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()
|
existing_user = db.query(User).filter(User.email == email).first()
|
||||||
if existing_user:
|
if existing_user:
|
||||||
raise ValueError("Email already registered")
|
raise ValueError("Email already registered")
|
||||||
|
|
||||||
# Hash password
|
|
||||||
hashed_password = self.hash_password(password)
|
hashed_password = self.hash_password(password)
|
||||||
|
|
||||||
# Create user (default role_id = 3 for customer)
|
|
||||||
user = User(
|
user = User(
|
||||||
full_name=name,
|
full_name=name,
|
||||||
email=email,
|
email=email,
|
||||||
password=hashed_password,
|
password=hashed_password,
|
||||||
phone=phone,
|
phone=phone,
|
||||||
role_id=3 # Customer role
|
role_id=3
|
||||||
)
|
)
|
||||||
db.add(user)
|
db.add(user)
|
||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(user)
|
db.refresh(user)
|
||||||
|
|
||||||
# Load role
|
|
||||||
user.role = db.query(Role).filter(Role.id == user.role_id).first()
|
user.role = db.query(Role).filter(Role.id == user.role_id).first()
|
||||||
|
|
||||||
# Generate tokens
|
|
||||||
tokens = self.generate_tokens(user.id)
|
tokens = self.generate_tokens(user.id)
|
||||||
|
|
||||||
# Save refresh token (expires in 7 days)
|
|
||||||
expires_at = datetime.utcnow() + timedelta(days=7)
|
expires_at = datetime.utcnow() + timedelta(days=7)
|
||||||
refresh_token = RefreshToken(
|
refresh_token = RefreshToken(
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
@@ -125,7 +112,6 @@ class AuthService:
|
|||||||
db.add(refresh_token)
|
db.add(refresh_token)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
# Send welcome email (non-blocking)
|
|
||||||
try:
|
try:
|
||||||
client_url = settings.CLIENT_URL or os.getenv("CLIENT_URL", "http://localhost:5173")
|
client_url = settings.CLIENT_URL or os.getenv("CLIENT_URL", "http://localhost:5173")
|
||||||
email_html = welcome_email_template(user.full_name, user.email, client_url)
|
email_html = welcome_email_template(user.full_name, user.email, client_url)
|
||||||
@@ -145,62 +131,52 @@ class AuthService:
|
|||||||
}
|
}
|
||||||
|
|
||||||
async def login(self, db: Session, email: str, password: str, remember_me: bool = False, mfa_token: str = None) -> dict:
|
async def login(self, db: Session, email: str, password: str, remember_me: bool = False, mfa_token: str = None) -> dict:
|
||||||
"""Login user with optional MFA verification"""
|
|
||||||
# Normalize email (lowercase and strip whitespace)
|
|
||||||
email = email.lower().strip() if email else ""
|
email = email.lower().strip() if email else ""
|
||||||
if not email:
|
if not email:
|
||||||
raise ValueError("Invalid email or password")
|
raise ValueError("Invalid email or password")
|
||||||
|
|
||||||
# Find user with role and password
|
|
||||||
user = db.query(User).filter(User.email == email).first()
|
user = db.query(User).filter(User.email == email).first()
|
||||||
if not user:
|
if not user:
|
||||||
logger.warning(f"Login attempt with non-existent email: {email}")
|
logger.warning(f"Login attempt with non-existent email: {email}")
|
||||||
raise ValueError("Invalid email or password")
|
raise ValueError("Invalid email or password")
|
||||||
|
|
||||||
# Check if user is active
|
|
||||||
if not user.is_active:
|
if not user.is_active:
|
||||||
logger.warning(f"Login attempt for inactive user: {email}")
|
logger.warning(f"Login attempt for inactive user: {email}")
|
||||||
raise ValueError("Account is disabled. Please contact support.")
|
raise ValueError("Account is disabled. Please contact support.")
|
||||||
|
|
||||||
# Load role
|
|
||||||
user.role = db.query(Role).filter(Role.id == user.role_id).first()
|
user.role = db.query(Role).filter(Role.id == user.role_id).first()
|
||||||
|
|
||||||
# Check password
|
|
||||||
if not self.verify_password(password, user.password):
|
if not self.verify_password(password, user.password):
|
||||||
logger.warning(f"Login attempt with invalid password for user: {email}")
|
logger.warning(f"Login attempt with invalid password for user: {email}")
|
||||||
raise ValueError("Invalid email or password")
|
raise ValueError("Invalid email or password")
|
||||||
|
|
||||||
# Check if MFA is enabled
|
|
||||||
if user.mfa_enabled:
|
if user.mfa_enabled:
|
||||||
if not mfa_token:
|
if not mfa_token:
|
||||||
# Return special response indicating MFA is required
|
|
||||||
return {
|
return {
|
||||||
"requires_mfa": True,
|
"requires_mfa": True,
|
||||||
"user_id": user.id
|
"user_id": user.id
|
||||||
}
|
}
|
||||||
|
|
||||||
# Verify MFA token
|
|
||||||
from ..services.mfa_service import mfa_service
|
from ..services.mfa_service import mfa_service
|
||||||
is_backup_code = len(mfa_token) == 8 # Backup codes are 8 characters
|
is_backup_code = len(mfa_token) == 8
|
||||||
if not mfa_service.verify_mfa(db, user.id, mfa_token, is_backup_code):
|
if not mfa_service.verify_mfa(db, user.id, mfa_token, is_backup_code):
|
||||||
raise ValueError("Invalid MFA token")
|
raise ValueError("Invalid MFA token")
|
||||||
|
|
||||||
# Generate tokens
|
|
||||||
tokens = self.generate_tokens(user.id)
|
tokens = self.generate_tokens(user.id)
|
||||||
|
|
||||||
# Calculate expiry based on remember_me
|
|
||||||
expiry_days = 7 if remember_me else 1
|
expiry_days = 7 if remember_me else 1
|
||||||
expires_at = datetime.utcnow() + timedelta(days=expiry_days)
|
expires_at = datetime.utcnow() + timedelta(days=expiry_days)
|
||||||
|
|
||||||
# Delete old/expired refresh tokens for this user to prevent duplicates
|
|
||||||
# This ensures we don't have multiple active tokens and prevents unique constraint violations
|
|
||||||
try:
|
try:
|
||||||
db.query(RefreshToken).filter(
|
db.query(RefreshToken).filter(
|
||||||
RefreshToken.user_id == user.id
|
RefreshToken.user_id == user.id
|
||||||
).delete()
|
).delete()
|
||||||
db.flush() # Flush to ensure deletion happens before insert
|
db.flush()
|
||||||
|
|
||||||
# Save new refresh token
|
|
||||||
refresh_token = RefreshToken(
|
refresh_token = RefreshToken(
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
token=tokens["refreshToken"],
|
token=tokens["refreshToken"],
|
||||||
@@ -211,7 +187,6 @@ class AuthService:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
logger.error(f"Error saving refresh token for user {user.id}: {str(e)}", exc_info=True)
|
logger.error(f"Error saving refresh token for user {user.id}: {str(e)}", exc_info=True)
|
||||||
# If there's still a duplicate, try to delete and retry once
|
|
||||||
try:
|
try:
|
||||||
db.query(RefreshToken).filter(
|
db.query(RefreshToken).filter(
|
||||||
RefreshToken.token == tokens["refreshToken"]
|
RefreshToken.token == tokens["refreshToken"]
|
||||||
@@ -236,14 +211,11 @@ class AuthService:
|
|||||||
}
|
}
|
||||||
|
|
||||||
async def refresh_access_token(self, db: Session, refresh_token_str: str) -> dict:
|
async def refresh_access_token(self, db: Session, refresh_token_str: str) -> dict:
|
||||||
"""Refresh access token"""
|
|
||||||
if not refresh_token_str:
|
if not refresh_token_str:
|
||||||
raise ValueError("Refresh token is required")
|
raise ValueError("Refresh token is required")
|
||||||
|
|
||||||
# Verify refresh token
|
|
||||||
decoded = self.verify_refresh_token(refresh_token_str)
|
decoded = self.verify_refresh_token(refresh_token_str)
|
||||||
|
|
||||||
# Check if refresh token exists in database
|
|
||||||
stored_token = db.query(RefreshToken).filter(
|
stored_token = db.query(RefreshToken).filter(
|
||||||
RefreshToken.token == refresh_token_str,
|
RefreshToken.token == refresh_token_str,
|
||||||
RefreshToken.user_id == decoded["userId"]
|
RefreshToken.user_id == decoded["userId"]
|
||||||
@@ -252,13 +224,11 @@ class AuthService:
|
|||||||
if not stored_token:
|
if not stored_token:
|
||||||
raise ValueError("Invalid refresh token")
|
raise ValueError("Invalid refresh token")
|
||||||
|
|
||||||
# Check if token is expired
|
|
||||||
if datetime.utcnow() > stored_token.expires_at:
|
if datetime.utcnow() > stored_token.expires_at:
|
||||||
db.delete(stored_token)
|
db.delete(stored_token)
|
||||||
db.commit()
|
db.commit()
|
||||||
raise ValueError("Refresh token expired")
|
raise ValueError("Refresh token expired")
|
||||||
|
|
||||||
# Generate new access token
|
|
||||||
access_token = jwt.encode(
|
access_token = jwt.encode(
|
||||||
{"userId": decoded["userId"]},
|
{"userId": decoded["userId"]},
|
||||||
self.jwt_secret,
|
self.jwt_secret,
|
||||||
@@ -268,19 +238,16 @@ class AuthService:
|
|||||||
return {"token": access_token}
|
return {"token": access_token}
|
||||||
|
|
||||||
async def logout(self, db: Session, refresh_token_str: str) -> bool:
|
async def logout(self, db: Session, refresh_token_str: str) -> bool:
|
||||||
"""Logout user"""
|
|
||||||
if refresh_token_str:
|
if refresh_token_str:
|
||||||
db.query(RefreshToken).filter(RefreshToken.token == refresh_token_str).delete()
|
db.query(RefreshToken).filter(RefreshToken.token == refresh_token_str).delete()
|
||||||
db.commit()
|
db.commit()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def get_profile(self, db: Session, user_id: int) -> dict:
|
async def get_profile(self, db: Session, user_id: int) -> dict:
|
||||||
"""Get user profile"""
|
|
||||||
user = db.query(User).filter(User.id == user_id).first()
|
user = db.query(User).filter(User.id == user_id).first()
|
||||||
if not user:
|
if not user:
|
||||||
raise ValueError("User not found")
|
raise ValueError("User not found")
|
||||||
|
|
||||||
# Load role
|
|
||||||
user.role = db.query(Role).filter(Role.id == user.role_id).first()
|
user.role = db.query(Role).filter(Role.id == user.role_id).first()
|
||||||
|
|
||||||
return self.format_user_response(user)
|
return self.format_user_response(user)
|
||||||
@@ -296,25 +263,22 @@ class AuthService:
|
|||||||
current_password: Optional[str] = None,
|
current_password: Optional[str] = None,
|
||||||
currency: Optional[str] = None
|
currency: Optional[str] = None
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Update user profile"""
|
|
||||||
user = db.query(User).filter(User.id == user_id).first()
|
user = db.query(User).filter(User.id == user_id).first()
|
||||||
if not user:
|
if not user:
|
||||||
raise ValueError("User not found")
|
raise ValueError("User not found")
|
||||||
|
|
||||||
# If password is being changed, verify current password
|
|
||||||
if password:
|
if password:
|
||||||
if not current_password:
|
if not current_password:
|
||||||
raise ValueError("Current password is required to change password")
|
raise ValueError("Current password is required to change password")
|
||||||
if not self.verify_password(current_password, user.password):
|
if not self.verify_password(current_password, user.password):
|
||||||
raise ValueError("Current password is incorrect")
|
raise ValueError("Current password is incorrect")
|
||||||
# Hash new password
|
|
||||||
user.password = self.hash_password(password)
|
user.password = self.hash_password(password)
|
||||||
|
|
||||||
# Update other fields
|
|
||||||
if full_name is not None:
|
if full_name is not None:
|
||||||
user.full_name = full_name
|
user.full_name = full_name
|
||||||
if email is not None:
|
if email is not None:
|
||||||
# Check if email is already taken by another user
|
|
||||||
existing_user = db.query(User).filter(
|
existing_user = db.query(User).filter(
|
||||||
User.email == email,
|
User.email == email,
|
||||||
User.id != user_id
|
User.id != user_id
|
||||||
@@ -325,7 +289,7 @@ class AuthService:
|
|||||||
if phone_number is not None:
|
if phone_number is not None:
|
||||||
user.phone = phone_number
|
user.phone = phone_number
|
||||||
if currency is not None:
|
if currency is not None:
|
||||||
# Validate currency code (ISO 4217, 3 characters)
|
|
||||||
if len(currency) == 3 and currency.isalpha():
|
if len(currency) == 3 and currency.isalpha():
|
||||||
user.currency = currency.upper()
|
user.currency = currency.upper()
|
||||||
else:
|
else:
|
||||||
@@ -334,36 +298,29 @@ class AuthService:
|
|||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(user)
|
db.refresh(user)
|
||||||
|
|
||||||
# Load role
|
|
||||||
user.role = db.query(Role).filter(Role.id == user.role_id).first()
|
user.role = db.query(Role).filter(Role.id == user.role_id).first()
|
||||||
|
|
||||||
return self.format_user_response(user)
|
return self.format_user_response(user)
|
||||||
|
|
||||||
def generate_reset_token(self) -> tuple:
|
def generate_reset_token(self) -> tuple:
|
||||||
"""Generate reset token"""
|
|
||||||
reset_token = secrets.token_hex(32)
|
reset_token = secrets.token_hex(32)
|
||||||
hashed_token = hashlib.sha256(reset_token.encode()).hexdigest()
|
hashed_token = hashlib.sha256(reset_token.encode()).hexdigest()
|
||||||
return reset_token, hashed_token
|
return reset_token, hashed_token
|
||||||
|
|
||||||
async def forgot_password(self, db: Session, email: str) -> dict:
|
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()
|
user = db.query(User).filter(User.email == email).first()
|
||||||
|
|
||||||
# Always return success to prevent email enumeration
|
|
||||||
if not user:
|
if not user:
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"message": "If email exists, reset link has been sent"
|
"message": "If email exists, reset link has been sent"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Generate reset token
|
|
||||||
reset_token, hashed_token = self.generate_reset_token()
|
reset_token, hashed_token = self.generate_reset_token()
|
||||||
|
|
||||||
# Delete old tokens
|
|
||||||
db.query(PasswordResetToken).filter(PasswordResetToken.user_id == user.id).delete()
|
db.query(PasswordResetToken).filter(PasswordResetToken.user_id == user.id).delete()
|
||||||
|
|
||||||
# Save token (expires in 1 hour)
|
|
||||||
expires_at = datetime.utcnow() + timedelta(hours=1)
|
expires_at = datetime.utcnow() + timedelta(hours=1)
|
||||||
reset_token_obj = PasswordResetToken(
|
reset_token_obj = PasswordResetToken(
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
@@ -373,31 +330,17 @@ class AuthService:
|
|||||||
db.add(reset_token_obj)
|
db.add(reset_token_obj)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
# Build reset URL
|
|
||||||
client_url = settings.CLIENT_URL or os.getenv("CLIENT_URL", "http://localhost:5173")
|
client_url = settings.CLIENT_URL or os.getenv("CLIENT_URL", "http://localhost:5173")
|
||||||
reset_url = f"{client_url}/reset-password/{reset_token}"
|
reset_url = f"{client_url}/reset-password/{reset_token}"
|
||||||
|
|
||||||
# Try to send email
|
|
||||||
try:
|
try:
|
||||||
logger.info(f"Attempting to send password reset email to {user.email}")
|
logger.info(f"Attempting to send password reset email to {user.email}")
|
||||||
logger.info(f"Reset URL: {reset_url}")
|
logger.info(f"Reset URL: {reset_url}")
|
||||||
email_html = password_reset_email_template(reset_url)
|
email_html = password_reset_email_template(reset_url)
|
||||||
|
|
||||||
# Create plain text version for better email deliverability
|
|
||||||
plain_text = f"""
|
|
||||||
Password Reset Request
|
|
||||||
|
|
||||||
You (or someone) has requested to reset your password for your Hotel Booking account.
|
plain_text = f
|
||||||
|
.strip()
|
||||||
Click the link below to reset your password. This link will expire in 1 hour:
|
|
||||||
|
|
||||||
{reset_url}
|
|
||||||
|
|
||||||
If you did not request this, please ignore this email.
|
|
||||||
|
|
||||||
Best regards,
|
|
||||||
Hotel Booking Team
|
|
||||||
""".strip()
|
|
||||||
|
|
||||||
await send_email(
|
await send_email(
|
||||||
to=user.email,
|
to=user.email,
|
||||||
@@ -408,7 +351,6 @@ Hotel Booking Team
|
|||||||
logger.info(f"Password reset email sent successfully to {user.email} with reset URL: {reset_url}")
|
logger.info(f"Password reset email sent successfully to {user.email} with reset URL: {reset_url}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to send password reset email to {user.email}: {type(e).__name__}: {str(e)}", exc_info=True)
|
logger.error(f"Failed to send password reset email to {user.email}: {type(e).__name__}: {str(e)}", exc_info=True)
|
||||||
# Still return success to prevent email enumeration, but log the error
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
@@ -416,14 +358,11 @@ Hotel Booking Team
|
|||||||
}
|
}
|
||||||
|
|
||||||
async def reset_password(self, db: Session, token: str, password: str) -> dict:
|
async def reset_password(self, db: Session, token: str, password: str) -> dict:
|
||||||
"""Reset Password - Update password with token"""
|
|
||||||
if not token or not password:
|
if not token or not password:
|
||||||
raise ValueError("Token and password are required")
|
raise ValueError("Token and password are required")
|
||||||
|
|
||||||
# Hash the token to compare
|
|
||||||
hashed_token = hashlib.sha256(token.encode()).hexdigest()
|
hashed_token = hashlib.sha256(token.encode()).hexdigest()
|
||||||
|
|
||||||
# Find valid token
|
|
||||||
reset_token = db.query(PasswordResetToken).filter(
|
reset_token = db.query(PasswordResetToken).filter(
|
||||||
PasswordResetToken.token == hashed_token,
|
PasswordResetToken.token == hashed_token,
|
||||||
PasswordResetToken.expires_at > datetime.utcnow(),
|
PasswordResetToken.expires_at > datetime.utcnow(),
|
||||||
@@ -433,27 +372,21 @@ Hotel Booking Team
|
|||||||
if not reset_token:
|
if not reset_token:
|
||||||
raise ValueError("Invalid or expired reset token")
|
raise ValueError("Invalid or expired reset token")
|
||||||
|
|
||||||
# Find user
|
|
||||||
user = db.query(User).filter(User.id == reset_token.user_id).first()
|
user = db.query(User).filter(User.id == reset_token.user_id).first()
|
||||||
if not user:
|
if not user:
|
||||||
raise ValueError("User not found")
|
raise ValueError("User not found")
|
||||||
|
|
||||||
# Check if new password matches old password
|
|
||||||
if self.verify_password(password, user.password):
|
if self.verify_password(password, user.password):
|
||||||
raise ValueError("New password must be different from the old password")
|
raise ValueError("New password must be different from the old password")
|
||||||
|
|
||||||
# Hash new password
|
|
||||||
hashed_password = self.hash_password(password)
|
hashed_password = self.hash_password(password)
|
||||||
|
|
||||||
# Update password
|
|
||||||
user.password = hashed_password
|
user.password = hashed_password
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
# Mark token as used
|
|
||||||
reset_token.used = True
|
reset_token.used = True
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
# Send confirmation email (non-blocking)
|
|
||||||
try:
|
try:
|
||||||
logger.info(f"Attempting to send password changed confirmation email to {user.email}")
|
logger.info(f"Attempting to send password changed confirmation email to {user.email}")
|
||||||
email_html = password_changed_email_template(user.email)
|
email_html = password_changed_email_template(user.email)
|
||||||
@@ -471,6 +404,5 @@ Hotel Booking Team
|
|||||||
"message": "Password has been reset successfully"
|
"message": "Password has been reset successfully"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
auth_service = AuthService()
|
auth_service = AuthService()
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user