Initial Commit

This commit is contained in:
belviskhoremk
2026-05-12 00:34:21 +00:00
commit d2dc43b16f
57 changed files with 6056 additions and 0 deletions

View 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")