feat: Space Booking System - MVP complet
Sistem web pentru rezervarea de birouri și săli de ședință cu flux de aprobare administrativă. Stack: FastAPI + Vue.js 3 + SQLite + TypeScript Features implementate: - Autentificare JWT + Self-registration cu email verification - CRUD Spații, Utilizatori, Settings (Admin) - Calendar interactiv (FullCalendar) cu drag-and-drop - Creare rezervări cu validare (durată, program, overlap, max/zi) - Rezervări recurente (săptămânal) - Admin: aprobare/respingere/anulare cereri - Admin: creare directă rezervări (bypass approval) - Admin: editare orice rezervare - User: editare/anulare rezervări proprii - Notificări in-app (bell icon + dropdown) - Notificări email (async SMTP cu BackgroundTasks) - Jurnal acțiuni administrative (audit log) - Rapoarte avansate (utilizare, top users, approval rate) - Șabloane rezervări (booking templates) - Atașamente fișiere (upload/download) - Conflict warnings (verificare disponibilitate real-time) - Integrare Google Calendar (OAuth2) - Suport timezone (UTC storage + user preference) - 225+ teste backend Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
112
backend/app/services/booking_service.py
Normal file
112
backend/app/services/booking_service.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""Booking validation service."""
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import and_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.booking import Booking
|
||||
from app.models.settings import Settings
|
||||
|
||||
|
||||
def validate_booking_rules(
|
||||
db: Session,
|
||||
space_id: int,
|
||||
user_id: int,
|
||||
start_datetime: datetime,
|
||||
end_datetime: datetime,
|
||||
exclude_booking_id: int | None = None,
|
||||
) -> list[str]:
|
||||
"""
|
||||
Validate booking against global settings rules.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
space_id: ID of the space to book
|
||||
user_id: ID of the user making the booking
|
||||
start_datetime: Booking start time
|
||||
end_datetime: Booking end time
|
||||
exclude_booking_id: Optional booking ID to exclude from overlap check
|
||||
(used when re-validating an existing booking)
|
||||
|
||||
Returns:
|
||||
List of validation error messages (empty list = validation OK)
|
||||
"""
|
||||
errors = []
|
||||
|
||||
# Fetch settings (create default if not exists)
|
||||
settings = db.query(Settings).filter(Settings.id == 1).first()
|
||||
if not settings:
|
||||
settings = Settings(
|
||||
id=1,
|
||||
min_duration_minutes=30,
|
||||
max_duration_minutes=480,
|
||||
working_hours_start=8,
|
||||
working_hours_end=20,
|
||||
max_bookings_per_day_per_user=3,
|
||||
min_hours_before_cancel=2,
|
||||
)
|
||||
db.add(settings)
|
||||
db.commit()
|
||||
db.refresh(settings)
|
||||
|
||||
# a) Validate duration in range
|
||||
duration_minutes = (end_datetime - start_datetime).total_seconds() / 60
|
||||
if (
|
||||
duration_minutes < settings.min_duration_minutes
|
||||
or duration_minutes > settings.max_duration_minutes
|
||||
):
|
||||
errors.append(
|
||||
f"Durata rezervării trebuie să fie între {settings.min_duration_minutes} "
|
||||
f"și {settings.max_duration_minutes} minute"
|
||||
)
|
||||
|
||||
# b) Validate working hours
|
||||
if (
|
||||
start_datetime.hour < settings.working_hours_start
|
||||
or end_datetime.hour > settings.working_hours_end
|
||||
):
|
||||
errors.append(
|
||||
f"Rezervările sunt permise doar între {settings.working_hours_start}:00 "
|
||||
f"și {settings.working_hours_end}:00"
|
||||
)
|
||||
|
||||
# c) Check for overlapping bookings
|
||||
query = db.query(Booking).filter(
|
||||
Booking.space_id == space_id,
|
||||
Booking.status.in_(["approved", "pending"]),
|
||||
and_(
|
||||
Booking.start_datetime < end_datetime,
|
||||
Booking.end_datetime > start_datetime,
|
||||
),
|
||||
)
|
||||
|
||||
# Exclude current booking if re-validating
|
||||
if exclude_booking_id is not None:
|
||||
query = query.filter(Booking.id != exclude_booking_id)
|
||||
|
||||
overlapping_bookings = query.first()
|
||||
if overlapping_bookings:
|
||||
errors.append("Spațiul este deja rezervat în acest interval")
|
||||
|
||||
# d) Check max bookings per day per user
|
||||
booking_date = start_datetime.date()
|
||||
start_of_day = datetime.combine(booking_date, datetime.min.time())
|
||||
end_of_day = datetime.combine(booking_date, datetime.max.time())
|
||||
|
||||
user_bookings_count = (
|
||||
db.query(Booking)
|
||||
.filter(
|
||||
Booking.user_id == user_id,
|
||||
Booking.status.in_(["approved", "pending"]),
|
||||
Booking.start_datetime >= start_of_day,
|
||||
Booking.start_datetime <= end_of_day,
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
if user_bookings_count >= settings.max_bookings_per_day_per_user:
|
||||
errors.append(
|
||||
f"Ai atins limita de {settings.max_bookings_per_day_per_user} rezervări pe zi"
|
||||
)
|
||||
|
||||
return errors
|
||||
Reference in New Issue
Block a user