feat: add multi-tenant system with properties, organizations, and public booking
Implement complete multi-property architecture: - Properties (groups of spaces) with public/private visibility - Property managers (many-to-many) with role-based permissions - Organizations with member management - Anonymous/guest booking support via public API (/api/public/*) - Property-scoped spaces, bookings, and settings - Frontend: property selector, organization management, public booking views - Migration script and updated seed data Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ from sqlalchemy import and_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.booking import Booking
|
||||
from app.models.property_settings import PropertySettings
|
||||
from app.models.settings import Settings
|
||||
from app.models.space import Space
|
||||
from app.utils.timezone import convert_from_utc, convert_to_utc
|
||||
@@ -53,27 +54,43 @@ def validate_booking_rules(
|
||||
db.commit()
|
||||
db.refresh(settings)
|
||||
|
||||
# Fetch space and get per-space settings (with fallback to global)
|
||||
# Fetch space and get per-space settings
|
||||
# Resolution chain: Space → PropertySettings → Global Settings
|
||||
space = db.query(Space).filter(Space.id == space_id).first()
|
||||
wh_start = (
|
||||
space.working_hours_start
|
||||
if space and space.working_hours_start is not None
|
||||
else settings.working_hours_start
|
||||
|
||||
# Fetch property settings if space has a property
|
||||
prop_settings = None
|
||||
if space and space.property_id:
|
||||
prop_settings = db.query(PropertySettings).filter(
|
||||
PropertySettings.property_id == space.property_id
|
||||
).first()
|
||||
|
||||
def resolve(space_val, prop_val, global_val):
|
||||
if space_val is not None:
|
||||
return space_val
|
||||
if prop_val is not None:
|
||||
return prop_val
|
||||
return global_val
|
||||
|
||||
wh_start = resolve(
|
||||
space.working_hours_start if space else None,
|
||||
prop_settings.working_hours_start if prop_settings else None,
|
||||
settings.working_hours_start,
|
||||
)
|
||||
wh_end = (
|
||||
space.working_hours_end
|
||||
if space and space.working_hours_end is not None
|
||||
else settings.working_hours_end
|
||||
wh_end = resolve(
|
||||
space.working_hours_end if space else None,
|
||||
prop_settings.working_hours_end if prop_settings else None,
|
||||
settings.working_hours_end,
|
||||
)
|
||||
min_dur = (
|
||||
space.min_duration_minutes
|
||||
if space and space.min_duration_minutes is not None
|
||||
else settings.min_duration_minutes
|
||||
min_dur = resolve(
|
||||
space.min_duration_minutes if space else None,
|
||||
prop_settings.min_duration_minutes if prop_settings else None,
|
||||
settings.min_duration_minutes,
|
||||
)
|
||||
max_dur = (
|
||||
space.max_duration_minutes
|
||||
if space and space.max_duration_minutes is not None
|
||||
else settings.max_duration_minutes
|
||||
max_dur = resolve(
|
||||
space.max_duration_minutes if space else None,
|
||||
prop_settings.max_duration_minutes if prop_settings else None,
|
||||
settings.max_duration_minutes,
|
||||
)
|
||||
|
||||
# Convert UTC times to user timezone for validation
|
||||
|
||||
@@ -128,6 +128,58 @@ Motiv: {reason}
|
||||
|
||||
Vă rugăm să contactați administratorul pentru detalii.
|
||||
|
||||
Cu stimă,
|
||||
Sistemul de Rezervări
|
||||
"""
|
||||
|
||||
elif event_type == "anonymous_created":
|
||||
guest_email = extra_data.get("guest_email", "N/A") if extra_data else "N/A"
|
||||
subject = "Cerere Anonimă de Rezervare"
|
||||
body = f"""Bună ziua,
|
||||
|
||||
O nouă cerere anonimă de rezervare necesită aprobarea dumneavoastră:
|
||||
|
||||
Persoana: {user_name}
|
||||
Email: {guest_email}
|
||||
Spațiu: {space_name}
|
||||
Data și ora: {start_str} - {end_str}
|
||||
Titlu: {booking.title}
|
||||
Descriere: {booking.description or 'N/A'}
|
||||
|
||||
Vă rugăm să accesați panoul de administrare pentru a aproba sau respinge această cerere.
|
||||
|
||||
Cu stimă,
|
||||
Sistemul de Rezervări
|
||||
"""
|
||||
|
||||
elif event_type == "anonymous_approved":
|
||||
subject = "Rezervare Aprobată"
|
||||
body = f"""Bună ziua {user_name},
|
||||
|
||||
Rezervarea dumneavoastră a fost aprobată:
|
||||
|
||||
Spațiu: {space_name}
|
||||
Data și ora: {start_str} - {end_str}
|
||||
Titlu: {booking.title}
|
||||
|
||||
Vă așteptăm!
|
||||
|
||||
Cu stimă,
|
||||
Sistemul de Rezervări
|
||||
"""
|
||||
|
||||
elif event_type == "anonymous_rejected":
|
||||
reason = extra_data.get("rejection_reason", "Nu a fost specificat") if extra_data else "Nu a fost specificat"
|
||||
subject = "Rezervare Respinsă"
|
||||
body = f"""Bună ziua {user_name},
|
||||
|
||||
Rezervarea dumneavoastră a fost respinsă:
|
||||
|
||||
Spațiu: {space_name}
|
||||
Data și ora: {start_str} - {end_str}
|
||||
Titlu: {booking.title}
|
||||
Motiv: {reason}
|
||||
|
||||
Cu stimă,
|
||||
Sistemul de Rezervări
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user