import { call, os } from "@orpc/server"; import { z } from "zod"; import { randomUUID } from "crypto"; import { createKV } from "../lib/create-kv.js"; const AvailabilitySchema = z.object({ id: z.string(), date: z.string(), // YYYY-MM-DD time: z.string(), // HH:MM durationMinutes: z.number().int().positive(), status: z.enum(["free", "reserved"]), reservedByBookingId: z.string().optional(), createdAt: z.string(), }); export type Availability = z.output; const kv = createKV("availability"); // Minimal Owner-Prüfung über Sessions/Users KV type Session = { id: string; userId: string; expiresAt: string; createdAt: string }; type User = { id: string; username: string; email: string; passwordHash: string; role: "customer" | "owner"; createdAt: string }; const sessionsKV = createKV("sessions"); const usersKV = createKV("users"); async function assertOwner(sessionId: string): Promise { const session = await sessionsKV.getItem(sessionId); if (!session) throw new Error("Invalid session"); if (new Date(session.expiresAt) < new Date()) throw new Error("Session expired"); const user = await usersKV.getItem(session.userId); if (!user || user.role !== "owner") throw new Error("Forbidden"); } const create = os .input( z.object({ sessionId: z.string(), date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), time: z.string().regex(/^\d{2}:\d{2}$/), durationMinutes: z.number().int().positive(), }) ) .handler(async ({ input }) => { try { await assertOwner(input.sessionId); // Prevent duplicate slot on same date+time const existing = await kv.getAllItems(); const conflict = existing.some((s) => s.date === input.date && s.time === input.time); if (conflict) { throw new Error("Es existiert bereits ein Slot zu diesem Datum und dieser Uhrzeit."); } const id = randomUUID(); const slot: Availability = { id, date: input.date, time: input.time, durationMinutes: input.durationMinutes, status: "free", createdAt: new Date().toISOString(), }; await kv.setItem(id, slot); return slot; } catch (err) { console.error("availability.create error", err); throw err; } }); const update = os .input(AvailabilitySchema.extend({ sessionId: z.string() })) .handler(async ({ input }) => { await assertOwner(input.sessionId); const { sessionId, ...rest } = input as any; await kv.setItem(rest.id, rest as Availability); return rest as Availability; }); const remove = os .input(z.object({ sessionId: z.string(), id: z.string() })) .handler(async ({ input }) => { await assertOwner(input.sessionId); const slot = await kv.getItem(input.id); if (slot && slot.status === "reserved") throw new Error("Cannot delete reserved slot"); await kv.removeItem(input.id); }); const list = os.handler(async () => { const allSlots = await kv.getAllItems(); // Auto-delete past slots const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD const now = new Date(); const currentTime = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`; let deletedCount = 0; const slotsToDelete: string[] = []; // Identify past slots for deletion allSlots.forEach(slot => { const isPastDate = slot.date < today; const isPastTime = slot.date === today && slot.time <= currentTime; if (isPastDate || isPastTime) { slotsToDelete.push(slot.id); } }); // Delete past slots (only if not reserved) for (const slotId of slotsToDelete) { const slot = await kv.getItem(slotId); if (slot && slot.status !== "reserved") { await kv.removeItem(slotId); deletedCount++; } } if (deletedCount > 0) { console.log(`Auto-deleted ${deletedCount} past availability slots`); } // Return remaining slots (all are now current/future) const remainingSlots = allSlots.filter(slot => !slotsToDelete.includes(slot.id)); return remainingSlots; }); const get = os.input(z.string()).handler(async ({ input }) => { return kv.getItem(input); }); const getByDate = os .input(z.string()) // YYYY-MM-DD .handler(async ({ input }) => { const all = await kv.getAllItems(); return all.filter((s) => s.date === input); }); // Cleanup function to manually delete past slots const cleanupPastSlots = os .input(z.object({ sessionId: z.string() })) .handler(async ({ input }) => { await assertOwner(input.sessionId); const allSlots = await kv.getAllItems(); const today = new Date().toISOString().split("T")[0]; const now = new Date(); const currentTime = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`; let deletedCount = 0; for (const slot of allSlots) { const isPastDate = slot.date < today; const isPastTime = slot.date === today && slot.time <= currentTime; if ((isPastDate || isPastTime) && slot.status !== "reserved") { await kv.removeItem(slot.id); deletedCount++; } } console.log(`Manual cleanup: deleted ${deletedCount} past availability slots`); return { deletedCount, message: `${deletedCount} vergangene Slots wurden gelöscht.` }; }); const live = { list: os.handler(async function* ({ signal }) { yield call(list, {}, { signal }); for await (const _ of kv.subscribe()) { yield call(list, {}, { signal }); } }), byDate: os .input(z.string()) .handler(async function* ({ input, signal }) { yield call(getByDate, input, { signal }); for await (const _ of kv.subscribe()) { yield call(getByDate, input, { signal }); } }), }; export const router = { create, update, remove, list, get, getByDate, cleanupPastSlots, live, };