mirror of
http://88.130.71.182:3000/BlitTech/badoHair_be.git
synced 2026-06-12 23:23:22 +00:00
Initial Commit
This commit is contained in:
175
app/services/slot_service.py
Normal file
175
app/services/slot_service.py
Normal file
@@ -0,0 +1,175 @@
|
||||
from datetime import date, timedelta, datetime, time
|
||||
|
||||
import asyncpg
|
||||
import pytz
|
||||
|
||||
from app.config import get_settings
|
||||
from app.exceptions import NotFoundError, AppError
|
||||
from app.models.bookings import WeeklyScheduleCreate, SlotCreate
|
||||
|
||||
|
||||
def _fmt_time(t) -> str:
|
||||
if t is None:
|
||||
return ""
|
||||
s = str(t)
|
||||
return s[:5]
|
||||
|
||||
|
||||
async def get_schedule(db: asyncpg.Connection) -> list[dict]:
|
||||
rows = await db.fetch("SELECT * FROM weekly_schedule WHERE is_active = true ORDER BY day_of_week, start_time")
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
async def create_schedule_entry(db: asyncpg.Connection, data: WeeklyScheduleCreate) -> dict:
|
||||
row = await db.fetchrow(
|
||||
"""
|
||||
INSERT INTO weekly_schedule (day_of_week, start_time, end_time, slot_duration_minutes)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING *
|
||||
""",
|
||||
data.day_of_week, data.start_time, data.end_time, data.slot_duration_minutes,
|
||||
)
|
||||
return dict(row)
|
||||
|
||||
|
||||
async def delete_schedule_entry(db: asyncpg.Connection, schedule_id: str):
|
||||
result = await db.execute("DELETE FROM weekly_schedule WHERE id = $1", schedule_id)
|
||||
if result == "DELETE 0":
|
||||
raise NotFoundError("schedule entry")
|
||||
|
||||
|
||||
async def generate_slots(db: asyncpg.Connection, from_date: date, to_date: date) -> int:
|
||||
if (to_date - from_date).days > 90:
|
||||
raise AppError("RANGE_TOO_LARGE", "Date range cannot exceed 90 days", 400)
|
||||
|
||||
schedule = await db.fetch("SELECT * FROM weekly_schedule WHERE is_active = true")
|
||||
if not schedule:
|
||||
raise AppError("NO_SCHEDULE", "No active weekly schedule configured", 400)
|
||||
|
||||
blocked = await db.fetch(
|
||||
"SELECT date FROM blocked_dates WHERE date BETWEEN $1 AND $2",
|
||||
from_date, to_date,
|
||||
)
|
||||
blocked_set = {r["date"] for r in blocked}
|
||||
|
||||
tz = pytz.timezone(get_settings().BUSINESS_TIMEZONE)
|
||||
created = 0
|
||||
current = from_date
|
||||
|
||||
async with db.transaction():
|
||||
while current <= to_date:
|
||||
if current in blocked_set:
|
||||
current += timedelta(days=1)
|
||||
continue
|
||||
|
||||
day_schedules = [s for s in schedule if s["day_of_week"] == current.weekday()]
|
||||
|
||||
for sched in day_schedules:
|
||||
cursor = datetime.combine(current, sched["start_time"])
|
||||
end_limit = datetime.combine(current, sched["end_time"])
|
||||
duration = timedelta(minutes=sched["slot_duration_minutes"])
|
||||
|
||||
while cursor + duration <= end_limit:
|
||||
slot_end = cursor + duration
|
||||
exists = await db.fetchval(
|
||||
"SELECT 1 FROM time_slots WHERE date = $1 AND start_time = $2",
|
||||
current, cursor.time(),
|
||||
)
|
||||
if not exists:
|
||||
await db.execute(
|
||||
"INSERT INTO time_slots (date, start_time, end_time) VALUES ($1, $2, $3)",
|
||||
current, cursor.time(), slot_end.time(),
|
||||
)
|
||||
created += 1
|
||||
cursor += duration
|
||||
|
||||
current += timedelta(days=1)
|
||||
|
||||
return created
|
||||
|
||||
|
||||
async def create_slot(db: asyncpg.Connection, data: SlotCreate) -> dict:
|
||||
if data.start_time >= data.end_time:
|
||||
raise AppError("INVALID_SLOT", "start_time must be before end_time", 400)
|
||||
row = await db.fetchrow(
|
||||
"INSERT INTO time_slots (date, start_time, end_time) VALUES ($1, $2, $3) RETURNING *",
|
||||
data.date, data.start_time, data.end_time,
|
||||
)
|
||||
return dict(row)
|
||||
|
||||
|
||||
async def delete_slot(db: asyncpg.Connection, slot_id: str):
|
||||
booked = await db.fetchval(
|
||||
"SELECT 1 FROM bookings WHERE slot_id = $1 AND status IN ('pending', 'confirmed')",
|
||||
slot_id,
|
||||
)
|
||||
if booked:
|
||||
raise AppError("SLOT_BOOKED", "Cannot delete a slot with active bookings", 409)
|
||||
result = await db.execute("DELETE FROM time_slots WHERE id = $1", slot_id)
|
||||
if result == "DELETE 0":
|
||||
raise NotFoundError("slot")
|
||||
|
||||
|
||||
async def update_slot(db: asyncpg.Connection, slot_id: str, is_blocked: bool, block_reason: str | None) -> dict:
|
||||
row = await db.fetchrow(
|
||||
"UPDATE time_slots SET is_blocked = $2, block_reason = $3 WHERE id = $1 RETURNING *",
|
||||
slot_id, is_blocked, block_reason,
|
||||
)
|
||||
if not row:
|
||||
raise NotFoundError("slot")
|
||||
return dict(row)
|
||||
|
||||
|
||||
async def list_slots(
|
||||
db: asyncpg.Connection,
|
||||
from_date: date,
|
||||
to_date: date,
|
||||
available_only: bool = False,
|
||||
) -> list[dict]:
|
||||
query = """
|
||||
SELECT
|
||||
ts.id, ts.date, ts.start_time, ts.end_time,
|
||||
ts.is_blocked, ts.block_reason,
|
||||
EXISTS(
|
||||
SELECT 1 FROM bookings b
|
||||
WHERE b.slot_id = ts.id AND b.status IN ('pending', 'confirmed')
|
||||
) AS is_booked
|
||||
FROM time_slots ts
|
||||
WHERE ts.date BETWEEN $1 AND $2
|
||||
"""
|
||||
params: list = [from_date, to_date]
|
||||
|
||||
if available_only:
|
||||
query += " AND ts.is_blocked = false"
|
||||
query += """
|
||||
AND NOT EXISTS(
|
||||
SELECT 1 FROM blocked_dates bd WHERE bd.date = ts.date
|
||||
)
|
||||
AND NOT EXISTS(
|
||||
SELECT 1 FROM bookings b WHERE b.slot_id = ts.id AND b.status IN ('pending', 'confirmed')
|
||||
)
|
||||
AND ts.date >= CURRENT_DATE
|
||||
"""
|
||||
|
||||
query += " ORDER BY ts.date, ts.start_time"
|
||||
rows = await db.fetch(query, *params)
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
async def get_blocked_dates(db: asyncpg.Connection) -> list[dict]:
|
||||
rows = await db.fetch("SELECT * FROM blocked_dates ORDER BY date")
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
async def add_blocked_date(db: asyncpg.Connection, date: date, reason: str | None) -> dict:
|
||||
row = await db.fetchrow(
|
||||
"INSERT INTO blocked_dates (date, reason) VALUES ($1, $2) ON CONFLICT (date) DO UPDATE SET reason = $2 RETURNING *",
|
||||
date, reason,
|
||||
)
|
||||
return dict(row)
|
||||
|
||||
|
||||
async def remove_blocked_date(db: asyncpg.Connection, blocked_date_id: str):
|
||||
result = await db.execute("DELETE FROM blocked_dates WHERE id = $1", blocked_date_id)
|
||||
if result == "DELETE 0":
|
||||
raise NotFoundError("blocked date")
|
||||
Reference in New Issue
Block a user