From 91afbd24f8346b22c601ac28cbe4b235500742ac Mon Sep 17 00:00:00 2001 From: Nicholai Date: Mon, 20 Oct 2025 14:34:55 -0600 Subject: [PATCH] feat: add global under-construction banner with session dismissal and booking phone\n\n- New components/construction-banner.tsx with amber warning style, tel link (719-698-9004), sessionStorage-based dismissal\n- Update app/ClientLayout.tsx to render banner, offset content via CSS var, and push fixed nav down --- app/ClientLayout.tsx | 10 +- app/globals.css | 11 - components/construction-banner.tsx | 110 +-- docs/BOOKING-WORKFLOW-FINAL-PLAN.md | 1144 +++++++++++++++++++++++++ docs/BOOKING-WORKFLOW-REVISED-PLAN.md | 724 ++++++++++++++++ docs/BOOKING-WORKFLOW-RISKS.md | 552 ++++++++++++ 6 files changed, 2488 insertions(+), 63 deletions(-) create mode 100644 docs/BOOKING-WORKFLOW-FINAL-PLAN.md create mode 100644 docs/BOOKING-WORKFLOW-REVISED-PLAN.md create mode 100644 docs/BOOKING-WORKFLOW-RISKS.md diff --git a/app/ClientLayout.tsx b/app/ClientLayout.tsx index cfcfad983..5eb7fee00 100644 --- a/app/ClientLayout.tsx +++ b/app/ClientLayout.tsx @@ -10,10 +10,10 @@ import { FeatureFlagsProvider } from "@/components/feature-flags-provider" import { LenisProvider } from "@/components/smooth-scroll-provider" import { Toaster } from "@/components/ui/sonner" import { ThemeProvider } from "@/components/theme-provider" -import { ConstructionBanner } from "@/components/construction-banner" import type { FlagsSnapshot } from "@/lib/flags" import "./globals.css" +import ConstructionBanner from "@/components/construction-banner" export default function ClientLayout({ children, @@ -51,10 +51,14 @@ export default function ClientLayout({ - Loading...}> - {children} + {/* Global construction banner */} + + {/* Push fixed nav down when banner visible */} + + {/* Offset page content by banner height */} +
{children}
diff --git a/app/globals.css b/app/globals.css index f774283a8..096ff6aff 100644 --- a/app/globals.css +++ b/app/globals.css @@ -3,17 +3,6 @@ @custom-variant dark (&:is(.dark *)); -/* Construction banner - fixed height */ -.construction-banner { - height: 60px; -} - -/* Push navigation down when banner is visible */ -body:has(.construction-banner) nav { - top: 60px !important; - transition: top 0.3s ease; -} - :root { /* Updated color tokens to match United Tattoo design brief */ --background: oklch(1 0 0); /* White */ diff --git a/components/construction-banner.tsx b/components/construction-banner.tsx index 7239b3ef8..871ea37af 100644 --- a/components/construction-banner.tsx +++ b/components/construction-banner.tsx @@ -1,69 +1,81 @@ "use client" -import { useState, useEffect } from "react" -import { AlertTriangle, X, Phone } from "lucide-react" -import { Button } from "@/components/ui/button" +import { useEffect, useRef, useState } from "react" -export function ConstructionBanner() { +export default function ConstructionBanner() { const [isVisible, setIsVisible] = useState(false) - const [isHydrated, setIsHydrated] = useState(false) + const bannerRef = useRef(null) + // Initialize from sessionStorage useEffect(() => { - // Check if banner was previously dismissed - const isDismissed = localStorage.getItem("construction-banner-dismissed") - setIsVisible(!isDismissed) - setIsHydrated(true) + try { + const dismissed = sessionStorage.getItem("constructionBannerDismissed") === "1" + setIsVisible(!dismissed) + } catch { + // If sessionStorage is unavailable, default to showing the banner + setIsVisible(true) + } }, []) - const handleDismiss = () => { - setIsVisible(false) - localStorage.setItem("construction-banner-dismissed", "true") - } + // Manage root class and CSS var for offset while visible + useEffect(() => { + const root = document.documentElement - // Don't render anything until hydrated to avoid mismatch - if (!isHydrated) { - return null - } + if (!isVisible) { + root.classList.remove("has-site-banner") + root.style.removeProperty("--site-banner-height") + return + } + + root.classList.add("has-site-banner") + + const updateBannerHeight = () => { + const height = bannerRef.current?.offsetHeight ?? 0 + root.style.setProperty("--site-banner-height", `${height}px`) + } + + updateBannerHeight() + window.addEventListener("resize", updateBannerHeight) + return () => { + window.removeEventListener("resize", updateBannerHeight) + } + }, [isVisible]) if (!isVisible) { return null } return ( -
-
-
-
- -

- Website Under Construction -

-
-
- β€’ -

- For bookings, please call: -

- - - (719) 698-9004 - -
- -
+
+
+ 🚧 Site Under Construction.{" "} + For bookings, call {" "} + + 719-698-9004 + +
) } + diff --git a/docs/BOOKING-WORKFLOW-FINAL-PLAN.md b/docs/BOOKING-WORKFLOW-FINAL-PLAN.md new file mode 100644 index 000000000..cf94cdfdf --- /dev/null +++ b/docs/BOOKING-WORKFLOW-FINAL-PLAN.md @@ -0,0 +1,1144 @@ +# Booking Workflow - Final Implementation Plan + +**Version:** 3.0 FINAL +**Date:** January 9, 2025 +**Status:** Ready for Implementation with All Decisions Made + +--- + +## βœ… Critical Decisions (LOCKED IN) + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| **Customer Auth** | ❌ NO LOGIN | Zero friction, book in 2 minutes | +| **Staff Auth** | βœ… Nextcloud OAuth2 | Already configured, SSO | +| **Email Service** | βœ… Resend | Free tier, easy, modern | +| **Monitoring** | βœ… Cloudflare + Sentry Free | Best of both worlds | +| **Artist Data** | βœ… D1 (existing) | NO MIGRATION NEEDED | +| **Artist Linking** | βœ… Via users.email | Simple FK join | +| **Timezone** | βœ… America/Denver | Hardcoded for Colorado | + +--- + +## 🎯 Authentication Architecture (FINAL) + +### Two-Tier System + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ CUSTOMERS (Public) β”‚ +β”‚ ❌ NO ACCOUNT REQUIRED β”‚ +β”‚ ❌ NO LOGIN β”‚ +β”‚ βœ… Just email + phone β”‚ +β”‚ βœ… Book anonymously β”‚ +β”‚ βœ… Receive confirmation via email β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ ARTISTS & ADMINS (Internal) β”‚ +β”‚ βœ… Nextcloud OAuth2 Login β”‚ +β”‚ βœ… Already configured! β”‚ +β”‚ βœ… Client ID: PZmqmi9vxYjtyWzt... β”‚ +β”‚ βœ… Single Sign-On β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## πŸ—„οΈ Artist Data Architecture (SOLVED!) + +### Current Schema (NO CHANGES NEEDED!) + +```sql +users table: + β”œβ”€ id TEXT (PK) + β”œβ”€ email TEXT (← LINK TO NEXTCLOUD) + β”œβ”€ name TEXT + β”œβ”€ role TEXT (ARTIST, SHOP_ADMIN, etc.) + └─ ... + +artists table: + β”œβ”€ id TEXT (PK) + β”œβ”€ user_id TEXT (FK β†’ users.id) ← JOIN HERE! + β”œβ”€ name TEXT + β”œβ”€ bio TEXT + β”œβ”€ specialties TEXT (JSON) + β”œβ”€ instagram_handle TEXT + β”œβ”€ is_active BOOLEAN + └─ ... + +artist_calendars table (NEW): + β”œβ”€ id TEXT (PK) + β”œβ”€ artist_id TEXT (FK β†’ artists.id) + β”œβ”€ calendar_url TEXT + └─ ... +``` + +### Linking Strategy + +```typescript +// To link artist to Nextcloud calendar: +const artist = await db.prepare(` + SELECT a.*, u.email + FROM artists a + JOIN users u ON a.user_id = u.id + WHERE a.id = ? +`).bind(artistId).first() + +// Now we have artist.email! +// This matches their Nextcloud user email +// Their calendar URL: portal.united-tattoos.com/remote.php/dav/calendars/{email}/personal/ +``` + +**No Migration Needed!** Just use the existing FK relationship. + +--- + +## πŸ‘₯ Artist Lifecycle Management + +### When Artist Joins + +**Option 1: Nextcloud First (Recommended)** +``` +1. Admin creates Nextcloud user + - Email: artist@email.com + - Group: "artist" + - Calendar created automatically + +2. Admin creates User in D1 + - Email: artist@email.com (matches Nextcloud) + - Role: ARTIST + +3. Admin creates Artist profile in D1 + - user_id: [user from step 2] + - Bio, specialties, etc. + +4. Admin links calendar + - Via /admin/calendars UI + - Auto-generates calendar URL from user email +``` + +**Option 2: D1 First** +``` +1. Admin creates User + Artist in D1 + +2. Admin creates Nextcloud user (manually) + - Must use SAME email as D1 user + +3. Admin links calendar via UI +``` + +### When Artist Leaves + +```sql +-- Don't delete! Just deactivate +UPDATE artists SET is_active = FALSE WHERE id = ?; +UPDATE users SET role = 'CLIENT' WHERE id = ?; + +-- Keep their: +βœ… Portfolio images (history) +βœ… Past appointments (records) +βœ… Calendar config (can reactivate) + +-- They lose: +❌ Access to admin dashboard +❌ Visibility on website +❌ Ability to receive new bookings +``` + +### Artist Updates + +``` +Name/Bio change: Update in D1 (web reflects immediately) +Email change: Update users.email AND Nextcloud email (both must match!) +Calendar change: Update via /admin/calendars UI +``` + +--- + +## πŸ” Nextcloud OAuth2 Configuration + +### Already Configured! + +**Nextcloud Details:** +- Base URL: `https://portal.united-tattoos.com` +- Client ID: `PZmqmi9vxYjtyWzt7f8QZk61jtwoAaqZ5bZz6wLvYUu4lYc0PPY6cx9qcBgDh5QI` +- Client Secret: `tkf7Ytc4vQII47OhumKBl3O3p6WhiPFQBzb5DJhw7ZjmJwDE0zTGwYGwF0MJjcsm` + +**Callback URLs (need to verify these are set in Nextcloud):** +- Dev: `http://localhost:3000/api/auth/callback/nextcloud` +- Prod: `https://united-tattoos.com/api/auth/callback/nextcloud` + +**User Info Endpoint:** +`https://portal.united-tattoos.com/ocs/v2.php/cloud/user?format=json` + +--- + +## πŸ“§ Resend Email Configuration + +### Credentials + +**API Key:** `re_NkMnKyNY_5eHUS1Ajj24GgmTNajHVeehQ` + +### DNS Configuration (Cloudflare) + +Add these records to `united-tattoos.com` in Cloudflare DNS: + +``` +1. Go to Resend Dashboard β†’ Domains β†’ Add united-tattoos.com +2. Resend will show you 3 DNS records to add +3. Add them to Cloudflare: + + Type: TXT + Name: resend._domainkey.united-tattoos.com + Value: [Resend will provide this - looks like "p=MIGfMA0GCSqGSIb3..."] + + Type: TXT + Name: _dmarc.united-tattoos.com + Value: v=DMARC1; p=none; + + Type: MX (if you want to receive emails) + Name: united-tattoos.com + Priority: 10 + Value: feedback-smtp.us-east-1.amazonses.com +``` + +**Sender Address:** `bookings@united-tattoos.com` + +--- + +## πŸ“‹ FINAL Implementation Plan + +## PHASE 0: Foundation (1-2 days) + +### 0.1 Database Migration for Anonymous Bookings + +**File: `sql/migrations/20250110_anonymous_bookings.sql`** + +```sql +-- Allow anonymous bookings - store customer info directly on appointment +ALTER TABLE appointments ADD COLUMN client_name TEXT; +ALTER TABLE appointments ADD COLUMN client_email TEXT; +ALTER TABLE appointments ADD COLUMN client_phone TEXT; + +-- client_id becomes optional (NULL for anonymous bookings) +-- In D1/SQLite we can't easily make FK optional, so we'll handle in app logic +``` + +### 0.2 Set Up Nextcloud OAuth2 Provider + +**File: `lib/auth.ts`** (UPDATE) + +```typescript +import NextAuth, { NextAuthOptions } from "next-auth" +import { getDB } from "./db" + +export const authOptions: NextAuthOptions = { + providers: [ + { + id: "nextcloud", + name: "Nextcloud", + type: "oauth", + authorization: { + url: "https://portal.united-tattoos.com/apps/oauth2/authorize", + params: { scope: "openid profile email" }, + }, + token: "https://portal.united-tattoos.com/apps/oauth2/api/v1/token", + userinfo: "https://portal.united-tattoos.com/ocs/v2.php/cloud/user?format=json", + clientId: process.env.NEXTCLOUD_CLIENT_ID, + clientSecret: process.env.NEXTCLOUD_CLIENT_SECRET, + profile(profile) { + // Nextcloud returns user info in ocs.data + return { + id: profile.ocs.data.id, + name: profile.ocs.data.displayname, + email: profile.ocs.data.email, + image: null, // Nextcloud avatar handling can be added later + } + }, + }, + ], + callbacks: { + async session({ session, token }) { + // Add user ID and role from database + if (session.user && token.email) { + const db = getDB() + const user = await db + .prepare('SELECT id, role FROM users WHERE email = ?') + .bind(token.email) + .first() + + if (user) { + session.user.id = user.id + session.user.role = user.role + } + } + + return session + }, + async signIn({ user, account, profile }) { + // Verify user is in allowed Nextcloud groups + // Could call Nextcloud API to check group membership + // For now, just allow anyone with Nextcloud account + return true + }, + }, + pages: { + signIn: '/auth/signin', + error: '/auth/error', + }, +} + +export const handler = NextAuth(authOptions) +``` + +**Environment Variables:** +```env +NEXTCLOUD_CLIENT_ID=PZmqmi9vxYjtyWzt7f8QZk61jtwoAaqZ5bZz6wLvYUu4lYc0PPY6cx9qcBgDh5QI +NEXTCLOUD_CLIENT_SECRET=tkf7Ytc4vQII47OhumKBl3O3p6WhiPFQBzb5DJhw7ZjmJwDE0zTGwYGwF0MJjcsm +NEXTCLOUD_BASE_URL=https://portal.united-tattoos.com +``` + +### 0.3 Create Admin Middleware + +**File: `middleware.ts`** (UPDATE) + +```typescript +import { withAuth } from "next-auth/middleware" +import { NextResponse } from "next/server" + +export default withAuth( + function middleware(req) { + const token = req.nextauth.token + const path = req.nextUrl.pathname + + // Admin routes require SHOP_ADMIN or SUPER_ADMIN + if (path.startsWith('/admin')) { + if (!token || (token.role !== 'SHOP_ADMIN' && token.role !== 'SUPER_ADMIN')) { + return NextResponse.redirect(new URL('/auth/signin', req.url)) + } + } + + // Artist dashboard requires ARTIST role + if (path.startsWith('/artist-dashboard')) { + if (!token || token.role !== 'ARTIST') { + return NextResponse.redirect(new URL('/auth/signin', req.url)) + } + } + + return NextResponse.next() + }, + { + callbacks: { + authorized: ({ token, req }) => { + const path = req.nextUrl.pathname + + // Public routes - no auth needed + if (path === '/' || + path.startsWith('/artists') || + path.startsWith('/book') || + path.startsWith('/aftercare') || + path.startsWith('/api/artists') || + path.startsWith('/api/appointments') && req.method === 'POST' || + path.startsWith('/api/caldav/availability')) { + return true + } + + // Protected routes + if (path.startsWith('/admin') || path.startsWith('/artist-dashboard')) { + return !!token + } + + // All other routes public + return true + }, + }, + } +) + +export const config = { + matcher: ['/admin/:path*', '/artist-dashboard/:path*'], +} +``` + +### 0.4 Set Up Resend Email Service + +**Install:** +```bash +npm install resend +``` + +**File: `lib/email.ts`** + +```typescript +import { Resend } from 'resend' + +const resend = new Resend(process.env.RESEND_API_KEY) +const FROM_EMAIL = 'United Tattoo ' + +export async function sendBookingConfirmationEmail(appointment: { + id: string + client_name: string + client_email: string + client_phone: string + artist_name: string + start_time: string + end_time: string + description: string +}) { + const startTime = new Date(appointment.start_time) + const formattedDate = new Intl.DateTimeFormat('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'America/Denver', + }).format(startTime) + + const formattedTime = new Intl.DateTimeFormat('en-US', { + hour: 'numeric', + minute: '2-digit', + timeZoneName: 'short', + timeZone: 'America/Denver', + }).format(startTime) + + try { + const { data, error } = await resend.emails.send({ + from: FROM_EMAIL, + to: appointment.client_email, + subject: '✨ Your Tattoo Booking Request - United Tattoo', + html: ` + + + + + + + + +
+

Booking Request Received! βœ…

+

We can't wait to create something amazing with you

+
+ +
+

Appointment Details

+ + + + + + + + + + + + + + + + + +
Artist:${appointment.artist_name}
Date:${formattedDate}
Time:${formattedTime}
Description:${appointment.description}
+
+ +
+

⏳ Pending Approval

+

Your request is being reviewed by ${appointment.artist_name}

+
+ +

What Happens Next?

+
    +
  1. Your artist will review your request and check their availability (usually within 24 hours)
  2. +
  3. You'll receive another email when your appointment is confirmed
  4. +
  5. Once confirmed, you can pay your deposit at the shop or via a secure payment link
  6. +
  7. Show up ready! Bring a valid ID and arrive 10 minutes early
  8. +
+ +
+

Need to Make Changes?

+

Contact us:

+

πŸ“ž (719) 555-1234

+

βœ‰οΈ info@united-tattoos.com

+

πŸ“ 123 Main St, Fountain, CO 80817

+
+ +
+

United Tattoo - Fountain, Colorado

+

You're receiving this because you requested an appointment

+
+ + + + `, + }) + + if (error) { + throw new Error(`Failed to send email: ${error.message}`) + } + + return data + } catch (error) { + console.error('Email send failed:', error) + // Don't throw - we don't want email failure to break booking + // Just log it and continue + return null + } +} + +export async function sendBookingStatusChangeEmail(appointment: { + id: string + client_name: string + client_email: string + artist_name: string + start_time: string + status: string + description: string +}) { + const startTime = new Date(appointment.start_time) + const formattedDate = new Intl.DateTimeFormat('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'America/Denver', + }).format(startTime) + + const formattedTime = new Intl.DateTimeFormat('en-US', { + hour: 'numeric', + minute: '2-digit', + timeZoneName: 'short', + timeZone: 'America/Denver', + }).format(startTime) + + const statusConfig = { + CONFIRMED: { + subject: 'βœ… Your Tattoo Appointment is Confirmed!', + title: 'Great News! Your Appointment is Confirmed', + message: `${appointment.artist_name} has confirmed your tattoo appointment!`, + action: 'You can pay your deposit at the shop or we\'ll send you a secure payment link separately.', + color: '#28a745', + }, + CANCELLED: { + subject: 'πŸ“… Appointment Update - United Tattoo', + title: 'Appointment Rescheduling Needed', + message: 'We need to reschedule your appointment due to a scheduling conflict.', + action: 'Please call us at (719) 555-1234 to find a new time that works for you.', + color: '#dc3545', + }, + } + + const config = statusConfig[appointment.status as keyof typeof statusConfig] + if (!config) return null // Don't send for other statuses + + try { + const { data, error } = await resend.emails.send({ + from: FROM_EMAIL, + to: appointment.client_email, + subject: config.subject, + html: ` + + + + +
+

${config.title}

+

${config.message}

+
+ +
+

Appointment Details

+ + + + + + + + + + + + + +
Artist:${appointment.artist_name}
Date:${formattedDate}
Time:${formattedTime}
+
+ +

${config.action}

+ +
+

Questions? Call (719) 555-1234

+

United Tattoo - Fountain, CO

+
+ + + + `, + }) + + if (error) { + console.error('Email error:', error) + return null + } + + return data + } catch (error) { + console.error('Status change email failed:', error) + return null + } +} + +// Send notification to artist when new booking received +export async function sendArtistBookingNotification(appointment: { + id: string + client_name: string + artist_email: string + artist_name: string + start_time: string + description: string +}) { + const startTime = new Date(appointment.start_time) + const formattedDate = new Intl.DateTimeFormat('en-US', { + weekday: 'long', + month: 'long', + day: 'numeric', + timeZone: 'America/Denver', + }).format(startTime) + + const formattedTime = new Intl.DateTimeFormat('en-US', { + hour: 'numeric', + minute: '2-digit', + timeZoneName: 'short', + timeZone: 'America/Denver', + }).format(startTime) + + try { + await resend.emails.send({ + from: FROM_EMAIL, + to: appointment.artist_email, + subject: 'πŸ”” New Booking Request', + html: ` + + + +

New Booking Request

+

You have a new tattoo appointment request:

+ +
+

Client: ${appointment.client_name}

+

Requested Date: ${formattedDate}

+

Requested Time: ${formattedTime}

+

Description: ${appointment.description}

+
+ +

Next Steps:

+
    +
  1. Check your Nextcloud calendar - this appears as "REQUEST: ${appointment.client_name}"
  2. +
  3. Log in to the admin dashboard to approve or reschedule
  4. +
  5. Or edit the calendar event in Nextcloud (remove "REQUEST:" to approve)
  6. +
+ +

+ + View in Dashboard + +

+ +

+ United Tattoo Admin System +

+ + + `, + }) + } catch (error) { + console.error('Artist notification email failed:', error) + return null + } +} +``` + +**Environment Variable:** +```env +RESEND_API_KEY=re_NkMnKyNY_5eHUS1Ajj24GgmTNajHVeehQ +``` + +### 0.5 Install Dependencies + +```bash +npm install resend date-fns-tz @sentry/nextjs +``` + +--- + +## PHASE 1: Anonymous Customer Booking (2-3 days) + +### 1.1 Update Appointments API for Anonymous Bookings + +**File: `app/api/appointments/route.ts`** (MAJOR UPDATE) + +```typescript +const createAppointmentSchema = z.object({ + artistId: z.string().min(1), + // NO clientId - this is anonymous! + clientName: z.string().min(2, "Name is required"), + clientEmail: z.string().email("Valid email is required"), + clientPhone: z.string().min(10, "Phone number is required"), + title: z.string().min(1), + description: z.string().optional(), + startTime: z.string().datetime(), + endTime: z.string().datetime(), + depositAmount: z.number().optional(), + notes: z.string().optional(), +}) + +export async function POST(request: NextRequest, ...) { + try { + // NO AUTHENTICATION CHECK - Public endpoint for customer bookings + + if (!Flags.BOOKING_ENABLED) { + return bookingDisabledResponse() + } + + const body = await request.json() + const validatedData = createAppointmentSchema.parse(body) + + const db = getDB(context?.env) + + // Check CalDAV availability (Nextcloud is source of truth) + const startDate = new Date(validatedData.startTime) + const endDate = new Date(validatedData.endTime) + + const availabilityCheck = await checkArtistAvailability( + validatedData.artistId, + startDate, + endDate, + context + ) + + if (!availabilityCheck.available) { + return NextResponse.json( + { + error: 'Time slot not available', + reason: availabilityCheck.reason || 'Selected time slot conflicts with existing booking.' + }, + { status: 409 } + ) + } + + // Create appointment with customer contact info (no user ID) + const appointmentId = crypto.randomUUID() + const insertStmt = db.prepare(` + INSERT INTO appointments ( + id, artist_id, client_id, client_name, client_email, client_phone, + title, description, start_time, end_time, + status, deposit_amount, notes, created_at, updated_at + ) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, 'PENDING', ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + `) + + await insertStmt.bind( + appointmentId, + validatedData.artistId, + validatedData.clientName, + validatedData.clientEmail, + validatedData.clientPhone, + validatedData.title, + validatedData.description || null, + validatedData.startTime, + validatedData.endTime, + validatedData.depositAmount || null, + validatedData.notes || null + ).run() + + // Fetch created appointment with artist info + const appointment = await db.prepare(` + SELECT + a.*, + ar.name as artist_name, + u.email as artist_email + FROM appointments a + JOIN artists ar ON a.artist_id = ar.id + JOIN users u ON ar.user_id = u.id + WHERE a.id = ? + `).bind(appointmentId).first() + + // Sync to CalDAV (non-blocking) + try { + await syncAppointmentToCalendar(appointment as any, context) + } catch (syncError) { + console.error('CalDAV sync failed:', syncError) + } + + // Send emails (non-blocking) + try { + // Email to customer + await sendBookingConfirmationEmail(appointment as any) + + // Email to artist + await sendArtistBookingNotification({ + id: appointment.id, + client_name: appointment.client_name, + artist_email: appointment.artist_email, + artist_name: appointment.artist_name, + start_time: appointment.start_time, + description: appointment.description, + }) + } catch (emailError) { + console.error('Email notification failed:', emailError) + } + + return NextResponse.json({ appointment }, { status: 201 }) + } catch (error) { + // ... error handling + } +} +``` + +### 1.2 Update Booking Form (Remove All Auth!) + +**File: `components/booking-form.tsx`** + +REMOVE these lines: +```typescript +// DELETE THIS: +import { useSession } from 'next-auth/react' +const { data: session } = useSession() + +// DELETE THIS: +if (!session?.user) { + toast.error('Please sign in') + router.push('/auth/signin') + return +} +``` + +UPDATE handleSubmit: +```typescript +const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + if (!bookingEnabled) { + toast.error('Booking temporarily unavailable') + return + } + + // NO SESSION CHECK! + + if (!selectedArtist || !appointmentStart || !appointmentEnd) { + toast.error('Please complete all required fields') + return + } + + createBooking.mutate({ + artistId: selectedArtist.id, + clientName: `${formData.firstName} ${formData.lastName}`, + clientEmail: formData.email, + clientPhone: formData.phone, + title: `Tattoo: ${formData.tattooDescription.substring(0, 50)}`, + description: formData.tattooDescription, + startTime: appointmentStart, + endTime: appointmentEnd, + depositAmount: formData.depositAmount, + notes: formData.specialRequests, + }, { + onSuccess: (data) => { + router.push(`/book/confirm/${data.appointment.id}`) + } + }) +} +``` + +### 1.3 Update Confirmation Page (Public, No Auth!) + +**File: `app/book/confirm/[id]/page.tsx`** + +```typescript +// NO getServerSession! +// NO authentication check! +// Page is PUBLIC - anyone with link can view + +async function getBooking(id: string) { + const db = getDB() + + const booking = await db.prepare(` + SELECT + a.*, + ar.name as artist_name, + ar.instagram_handle + FROM appointments a + JOIN artists ar ON a.artist_id = ar.id + WHERE a.id = ? + `).bind(id).first() + + return booking +} + +export default async function BookingConfirmationPage({ params }) { + const booking = await getBooking(params.id) + + if (!booking) { + notFound() + } + + // Uses client_name, client_email from appointment (no user table needed!) + // ... rest of confirmation page +} +``` + +--- + +## πŸ”„ Artist Lifecycle Workflows + +### New Artist Joins + +**Step 1: Create in Nextcloud** +``` +Admin β†’ Nextcloud Users β†’ Add User + Name: Artist Name + Email: artist@email.com + Groups: [artist] + β†’ Nextcloud auto-creates personal calendar +``` + +**Step 2: Create in D1** +```sql +-- Create user record +INSERT INTO users (id, email, name, role, created_at) +VALUES ('uuid', 'artist@email.com', 'Artist Name', 'ARTIST', CURRENT_TIMESTAMP); + +-- Create artist profile +INSERT INTO artists (id, user_id, name, bio, specialties, is_active, created_at) +VALUES ('uuid', 'user_id_from_above', 'Artist Name', 'Bio...', '["Style1","Style2"]', TRUE, CURRENT_TIMESTAMP); +``` + +**Step 3: Link Calendar** +``` +Admin β†’ /admin/calendars β†’ Configure + Artist: [Select from dropdown] + Calendar URL: https://portal.united-tattoos.com/remote.php/dav/calendars/artist@email.com/personal/ + β†’ Saves to artist_calendars table +``` + +### Artist Leaves + +```sql +-- Deactivate (don't delete - preserve history) +UPDATE artists SET is_active = FALSE WHERE id = 'artist_id'; + +-- Optional: Change user role so they can't access admin +UPDATE users SET role = 'CLIENT' WHERE id = 'user_id'; + +-- Calendar config stays (for historical bookings) +-- Portfolio images stay (preserved history) +-- Past appointments stay (records) +``` + +**Result:** +- βœ… Artist disappears from website +- βœ… Can't receive new bookings +- βœ… Can't log into admin dashboard +- βœ… Historical data preserved +- βœ… Can reactivate later if they return + +### Artist Email Changes + +```sql +-- Update in BOTH places +UPDATE users SET email = 'new@email.com' WHERE id = 'user_id'; + +-- Nextcloud: Admin must change email there too +-- Then update calendar URL: +UPDATE artist_calendars +SET calendar_url = 'https://portal.united-tattoos.com/remote.php/dav/calendars/new@email.com/personal/' +WHERE artist_id = 'artist_id'; +``` + +**Important:** Email must match in both systems! + +--- + +## 🎯 Complete Workflow Diagram + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ CUSTOMER JOURNEY (No Login!) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +1. Visit united-tattoos.com/book +2. Select artist (from D1 database) +3. Fill form: name, email, phone, tattoo details +4. Select date/time β†’ Real-time availability check (CalDAV) +5. Submit β†’ Creates PENDING appointment in D1 +6. Redirect to /book/confirm/[id] (public page) +7. Receive confirmation email via Resend + +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ SYSTEM AUTOMATION β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +8. Appointment syncs to Nextcloud calendar + Title: "REQUEST: John Doe - Dragon Tattoo" +9. Artist receives email notification +10. Artist sees request in Nextcloud calendar app + +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ ARTIST/ADMIN APPROVAL (Via Web) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +11. Artist/admin logs in via Nextcloud OAuth2 +12. Views /admin/bookings dashboard +13. Clicks "Approve" on pending request +14. Status β†’ CONFIRMED in D1 +15. Syncs to Nextcloud (removes "REQUEST:" prefix) +16. Customer receives "Confirmed" email + +OR + +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ ARTIST/ADMIN APPROVAL (Via Nextcloud) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +11. Artist opens Nextcloud calendar on phone/desktop +12. Sees "REQUEST: John Doe - Dragon Tattoo" +13. Edits event, removes "REQUEST:" prefix +14. Saves event +15. Background worker (every 5 min) detects change +16. Status β†’ CONFIRMED in D1 +17. Customer receives "Confirmed" email +``` + +--- + +## 🎯 Implementation Order (FINAL) + +### Phase 0: Foundation (Days 1-2) +- [ ] Run database migration for anonymous bookings +- [ ] Set up Resend email (add DNS records) +- [ ] Test email sending +- [ ] Configure Nextcloud OAuth2 in NextAuth +- [ ] Test OAuth2 login +- [ ] Create admin middleware +- [ ] Install Sentry (optional) + +### Phase 1: Customer Booking (Days 3-5) +- [ ] Create use-bookings.ts hook +- [ ] Update booking form (remove auth, add API call) +- [ ] Update appointments API for anonymous bookings +- [ ] Create confirmation page (public) +- [ ] Test full booking flow + +### Phase 2: Admin Dashboards (Days 6-9) +- [ ] Create use-calendar-configs.ts hook +- [ ] Build calendar configuration UI +- [ ] Build bookings dashboard with DataTable +- [ ] Add approve/reject functionality +- [ ] Test admin workflows + +### Phase 3: Background Sync (Days 10-12) +- [ ] Add status detection to calendar-sync.ts +- [ ] Create background sync worker +- [ ] Configure wrangler.toml with cron +- [ ] Deploy worker +- [ ] Test Nextcloud β†’ Web sync + +### Phase 4: Testing & Deploy (Days 13-15) +- [ ] End-to-end testing +- [ ] Load testing +- [ ] Deploy to production +- [ ] Monitor for 48 hours + +--- + +## πŸ”§ Artist Linking - HOW IT WORKS + +### The Connection Chain + +``` +Website Booking Form: + ↓ + Selects artist (from D1 artists table) + ↓ + Artist has user_id (FK to users table) + ↓ + Users table has email + ↓ + Email matches Nextcloud user + ↓ + Nextcloud user has calendar + ↓ + artist_calendars table links artist.id β†’ calendar URL +``` + +### Example Query + +```sql +-- Get artist with calendar info +SELECT + a.id as artist_id, + a.name as artist_name, + u.email as artist_email, + ac.calendar_url +FROM artists a +JOIN users u ON a.user_id = u.id +LEFT JOIN artist_calendars ac ON a.id = ac.artist_id +WHERE a.id = ?; +``` + +**Result:** +``` +artist_id: "uuid-123" +artist_name: "Christy Lumberg" +artist_email: "christy@example.com" ← Links to Nextcloud! +calendar_url: "https://portal.united-tattoos.com/remote.php/dav/calendars/christy@example.com/personal/" +``` + +--- + +## πŸ“ Pre-Implementation Checklist + +### βœ… You Need To Do: + +1. **Set up Resend domain** (15 minutes) + - Go to resend.com/domains + - Add united-tattoos.com + - Copy the 3 DNS records + - Add to Cloudflare DNS + - Verify domain + +2. **Verify Nextcloud OAuth2 callback URLs** (2 minutes) + - Log into portal.united-tattoos.com + - Settings β†’ Security β†’ OAuth 2.0 + - Check callback URLs include: + - `https://united-tattoos.com/api/auth/callback/nextcloud` + - `http://localhost:3000/api/auth/callback/nextcloud` + +3. **Verify users table has emails** (1 minute) + ```sql + -- Run in Wrangler D1: + SELECT u.email, a.name + FROM artists a + JOIN users u ON a.user_id = u.id + WHERE a.is_active = TRUE; + ``` + - Should show all artists with their emails + - These emails MUST match Nextcloud user emails + +4. **Optional: Set up Sentry** (10 minutes) + - Go to sentry.io + - Create project + - Copy DSN + - Add to environment variables + +--- + +## πŸš€ Ready to Implement? + +I have everything I need: +- βœ… Resend API key +- βœ… Nextcloud OAuth2 credentials +- βœ… Domain ownership confirmed +- βœ… Architecture decided + +**Just need confirmation on:** +1. Are there emails for all current artists in the users table? +2. Do those emails match their Nextcloud accounts? + +Once confirmed, I'll start Phase 0! + diff --git a/docs/BOOKING-WORKFLOW-REVISED-PLAN.md b/docs/BOOKING-WORKFLOW-REVISED-PLAN.md new file mode 100644 index 000000000..675080bd0 --- /dev/null +++ b/docs/BOOKING-WORKFLOW-REVISED-PLAN.md @@ -0,0 +1,724 @@ +# Booking Workflow - Revised Implementation Plan + +**Version:** 2.0 (Revised with Nextcloud OAuth2) +**Date:** January 9, 2025 +**Status:** Ready for Implementation + +--- + +## Critical Architectural Decisions + +### 1. Authentication Strategy: TWO-TIER SYSTEM + +**Customers (Public):** +- ❌ NO LOGIN REQUIRED +- βœ… Anonymous booking with email + phone +- βœ… Receive confirmation via email +- βœ… Simple, fast, no friction + +**Artists & Admins (Internal):** +- βœ… Nextcloud OAuth2 authentication +- βœ… Single sign-on (SSO) +- βœ… Access to admin dashboards + calendars + +### 2. Artist Data: HYBRID MODEL (No Migration Needed!) + +**Cloudflare D1 + R2:** +- βœ… Source of truth for artist PROFILES +- βœ… Name, bio, specialties, portfolio images +- βœ… Used by public website (existing code stays as-is) +- βœ… Booking form artist selection +- βœ… **NO CHANGES TO EXISTING SYSTEM** + +**Nextcloud:** +- βœ… Source of truth for AUTHENTICATION +- βœ… Source of truth for CALENDAR availability +- βœ… Artists are users in "artist" Nextcloud group + +**Link Between Them:** +- Simple: Match via email address +- Robust: Add optional `nextcloud_user_id` to artists table +- No complex sync needed! + +### 3. Services Confirmed + +**Email:** Resend (free tier - 3,000/month) +- Domain: `united-tattoos.com` (owned, on Cloudflare) +- Sender: `bookings@united-tattoos.com` +- Easy ownership transfer via team feature + +**Monitoring:** +- Cloudflare Workers Analytics (free, built-in) +- Sentry (free tier - 5k errors/month) + +**Authentication:** +- Nextcloud OAuth2 (already configured!) +- Client ID: `PZmqmi9vxYjtyWzt7f8QZk61jtwoAaqZ5bZz6wLvYUu4lYc0PPY6cx9qcBgDh5QI` +- Secret: `tkf7Ytc4vQII47OhumKBl3O3p6WhiPFQBzb5DJhw7ZjmJwDE0zTGwYGwF0MJjcsm` +- Base URL: `https://portal.united-tattoos.com` + +--- + +## Revised Implementation Phases + +## PHASE 0: Foundation Setup (NEW - FIRST!) + +**Duration:** 1-2 days +**Priority:** BLOCKING - Must complete before other phases + +### 0.1 Set Up Resend Email Service + +**Steps:** +1. Sign up at resend.com (free tier) +2. Add domain `united-tattoos.com` +3. Add DNS records to Cloudflare: + ``` + TXT resend._domainkey [value from Resend] + TXT _dmarc "v=DMARC1; p=none;" + ``` +4. Verify domain +5. Test send email +6. Add API key to environment variables + +**Environment Variable:** +```env +RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxxxxxxxxxx +``` + +### 0.2 Configure Nextcloud OAuth2 Provider + +**Already Done! Just need to integrate:** + +**Update `lib/auth.ts` (NextAuth config):** + +```typescript +import NextAuth, { NextAuthOptions } from "next-auth" +import { D1Adapter } from "@next-auth/d1-adapter" + +export const authOptions: NextAuthOptions = { + adapter: D1Adapter(process.env.DB), // Existing D1 adapter + providers: [ + { + id: "nextcloud", + name: "Nextcloud", + type: "oauth", + authorization: { + url: "https://portal.united-tattoos.com/apps/oauth2/authorize", + params: { scope: "openid profile email" }, + }, + token: "https://portal.united-tattoos.com/apps/oauth2/api/v1/token", + userinfo: "https://portal.united-tattoos.com/ocs/v2.php/cloud/user?format=json", + clientId: process.env.NEXTCLOUD_CLIENT_ID, + clientSecret: process.env.NEXTCLOUD_CLIENT_SECRET, + profile(profile) { + return { + id: profile.ocs.data.id, + name: profile.ocs.data.displayname, + email: profile.ocs.data.email, + image: profile.ocs.data.avatar || null, + } + }, + }, + ], + callbacks: { + async session({ session, user }) { + // Add user role from database + const db = getDB() + const dbUser = await db + .prepare('SELECT role FROM users WHERE email = ?') + .bind(session.user.email) + .first() + + session.user.id = user.id + session.user.role = dbUser?.role || 'CLIENT' + + return session + }, + async signIn({ user, account, profile }) { + // Check if user is in Nextcloud "artist" or "admin" group + // This can be checked via Nextcloud API if needed + return true + }, + }, + pages: { + signIn: '/auth/signin', + error: '/auth/error', + }, +} +``` + +**Environment Variables:** +```env +NEXTCLOUD_CLIENT_ID=PZmqmi9vxYjtyWzt7f8QZk61jtwoAaqZ5bZz6wLvYUu4lYc0PPY6cx9qcBgDh5QI +NEXTCLOUD_CLIENT_SECRET=tkf7Ytc4vQII47OhumKBl3O3p6WhiPFQBzb5DJhw7ZjmJwDE0zTGwYGwF0MJjcsm +NEXTCLOUD_BASE_URL=https://portal.united-tattoos.com +``` + +**Callback URL (already configured in Nextcloud):** +- Production: `https://united-tattoos.com/api/auth/callback/nextcloud` +- Dev: `http://localhost:3000/api/auth/callback/nextcloud` + +### 0.3 Create Admin Middleware + +**File: `middleware.ts`** (Update existing or create) + +```typescript +import { withAuth } from "next-auth/middleware" +import { NextResponse } from "next/server" + +export default withAuth( + function middleware(req) { + const token = req.nextauth.token + const path = req.nextUrl.pathname + + // Admin routes require SHOP_ADMIN or SUPER_ADMIN role + if (path.startsWith('/admin')) { + if (!token || (token.role !== 'SHOP_ADMIN' && token.role !== 'SUPER_ADMIN')) { + return NextResponse.redirect(new URL('/auth/signin?callbackUrl=' + path, req.url)) + } + } + + // Artist dashboard requires ARTIST role + if (path.startsWith('/artist-dashboard')) { + if (!token || token.role !== 'ARTIST') { + return NextResponse.redirect(new URL('/auth/signin?callbackUrl=' + path, req.url)) + } + } + + return NextResponse.next() + }, + { + callbacks: { + authorized: ({ token, req }) => { + // Allow all API routes (they handle their own auth) + if (req.nextUrl.pathname.startsWith('/api')) { + return true + } + + // Require auth for admin/artist routes + if (req.nextUrl.pathname.startsWith('/admin') || + req.nextUrl.pathname.startsWith('/artist-dashboard')) { + return !!token + } + + // All other routes are public + return true + }, + }, + } +) + +export const config = { + matcher: ['/admin/:path*', '/artist-dashboard/:path*', '/api/:path*'], +} +``` + +### 0.4 Add Sentry Error Tracking + +**Install:** +```bash +npm install @sentry/nextjs +``` + +**File: `sentry.client.config.js`** +```javascript +import * as Sentry from "@sentry/nextjs" + +Sentry.init({ + dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, + tracesSampleRate: 0.1, + environment: process.env.NODE_ENV, +}) +``` + +**Environment Variable:** +```env +NEXT_PUBLIC_SENTRY_DSN=https://xxxxx@xxxxx.ingest.sentry.io/xxxxx +``` + +--- + +## PHASE 1: Customer Booking Flow (REVISED) + +**Duration:** 3-4 days +**Changes:** No customer authentication required! + +### 1.1 Update Database Schema for Anonymous Bookings + +**Add to migrations:** +```sql +-- Allow client_id to be NULL for anonymous bookings +-- Store email/phone directly on appointment +ALTER TABLE appointments ADD COLUMN client_email TEXT; +ALTER TABLE appointments ADD COLUMN client_phone TEXT; +ALTER TABLE appointments ADD COLUMN client_name TEXT; + +-- Make client_id optional +-- (In SQLite, we'd need to recreate the table, but for now just allow NULL in app logic) +``` + +### 1.2 Update Booking Form (No Login Required!) + +**File: `components/booking-form.tsx`** + +Remove all session checks! Update to: + +```typescript +const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + if (!bookingEnabled) { + toast.error('Booking temporarily unavailable') + return + } + + // NO SESSION CHECK - Customers book anonymously! + + if (!selectedArtist) { + toast.error('Please select an artist') + return + } + + if (!appointmentStart || !appointmentEnd) { + toast.error('Please select a date, time, and tattoo size') + return + } + + // Create booking without authentication + createBooking.mutate({ + artistId: selectedArtist.id, + // No clientId - this is anonymous! + clientName: `${formData.firstName} ${formData.lastName}`, + clientEmail: formData.email, + clientPhone: formData.phone, + title: `Tattoo: ${formData.tattooDescription.substring(0, 50)}`, + description: formData.tattooDescription, + startTime: appointmentStart, + endTime: appointmentEnd, + depositAmount: formData.depositAmount, + notes: formData.specialRequests, + }, { + onSuccess: (data) => { + router.push(`/book/confirm/${data.appointment.id}`) + } + }) +} +``` + +### 1.3 Update Appointments API for Anonymous Bookings + +**File: `app/api/appointments/route.ts`** (UPDATE) + +```typescript +const createAppointmentSchema = z.object({ + artistId: z.string().min(1), + clientId: z.string().optional(), // Now optional! + clientName: z.string().min(1), // NEW - Required + clientEmail: z.string().email(), // NEW - Required + clientPhone: z.string().min(1), // NEW - Required + title: z.string().min(1), + description: z.string().optional(), + startTime: z.string().datetime(), + endTime: z.string().datetime(), + depositAmount: z.number().optional(), + totalAmount: z.number().optional(), + notes: z.string().optional(), +}) + +export async function POST(request: NextRequest, ...) { + try { + // NO AUTHENTICATION CHECK for booking creation! + // This is intentionally public for customer bookings + + const body = await request.json() + const validatedData = createAppointmentSchema.parse(body) + + const db = getDB(context?.env) + + // Check CalDAV availability (Nextcloud is source of truth) + const startDate = new Date(validatedData.startTime) + const endDate = new Date(validatedData.endTime) + + const availabilityCheck = await checkArtistAvailability( + validatedData.artistId, + startDate, + endDate, + context + ) + + if (!availabilityCheck.available) { + return NextResponse.json( + { + error: 'Time slot not available', + reason: availabilityCheck.reason || 'Selected time slot conflicts with existing booking.' + }, + { status: 409 } + ) + } + + const appointmentId = crypto.randomUUID() + + // Create appointment with customer contact info + const insertStmt = db.prepare(` + INSERT INTO appointments ( + id, artist_id, client_id, client_name, client_email, client_phone, + title, description, start_time, end_time, + status, deposit_amount, total_amount, notes, created_at, updated_at + ) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, 'PENDING', ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + `) + + await insertStmt.bind( + appointmentId, + validatedData.artistId, + validatedData.clientName, + validatedData.clientEmail, + validatedData.clientPhone, + validatedData.title, + validatedData.description || null, + validatedData.startTime, + validatedData.endTime, + validatedData.depositAmount || null, + validatedData.totalAmount || null, + validatedData.notes || null + ).run() + + // Fetch the created appointment + const appointment = await db.prepare(` + SELECT + a.*, + ar.name as artist_name + FROM appointments a + JOIN artists ar ON a.artist_id = ar.id + WHERE a.id = ? + `).bind(appointmentId).first() + + // Sync to CalDAV + try { + await syncAppointmentToCalendar(appointment as any, context) + } catch (syncError) { + console.error('Failed to sync to calendar:', syncError) + } + + // Send email confirmation to customer + try { + await sendBookingConfirmationEmail(appointment as any) + } catch (emailError) { + console.error('Failed to send confirmation email:', emailError) + } + + return NextResponse.json({ appointment }, { status: 201 }) + } catch (error) { + // ... error handling + } +} +``` + +### 1.4 Create Email Service + +**File: `lib/email.ts`** + +```typescript +import { Resend } from 'resend' + +const resend = new Resend(process.env.RESEND_API_KEY) + +export async function sendBookingConfirmationEmail(appointment: { + id: string + client_name: string + client_email: string + artist_name: string + start_time: string + description: string +}) { + const startTime = new Date(appointment.start_time) + const formattedDate = new Intl.DateTimeFormat('en-US', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + timeZone: 'America/Denver', + }).format(startTime) + + const formattedTime = new Intl.DateTimeFormat('en-US', { + hour: 'numeric', + minute: '2-digit', + timeZoneName: 'short', + timeZone: 'America/Denver', + }).format(startTime) + + try { + await resend.emails.send({ + from: 'United Tattoo ', + to: appointment.client_email, + subject: 'Your Tattoo Booking Request - United Tattoo', + html: ` +

Booking Request Received!

+

Hi ${appointment.client_name},

+

We've received your tattoo appointment request. Here are the details:

+ +
+

Artist: ${appointment.artist_name}

+

Date: ${formattedDate}

+

Time: ${formattedTime}

+

Description: ${appointment.description}

+
+ +

What's Next?

+
    +
  1. Your artist will review your request (usually within 24 hours)
  2. +
  3. You'll receive an email when your appointment is confirmed
  4. +
  5. Bring a valid ID and arrive 10 minutes early
  6. +
+ +

Questions? Call us at (719) 555-1234 or reply to this email.

+ +

Thanks,
United Tattoo Team

+ `, + }) + } catch (error) { + console.error('Email send failed:', error) + throw error + } +} + +export async function sendBookingStatusChangeEmail(appointment: { + client_name: string + client_email: string + artist_name: string + start_time: string + status: string +}) { + const statusMessages = { + CONFIRMED: { + subject: 'Your Tattoo Appointment is Confirmed!', + message: 'Great news! Your appointment has been confirmed.', + action: 'You can now pay your deposit at the shop or via the link below.', + }, + CANCELLED: { + subject: 'Appointment Update - United Tattoo', + message: 'Unfortunately, we need to reschedule your appointment.', + action: 'Please contact us to find a new time that works for you.', + }, + } + + const config = statusMessages[appointment.status as keyof typeof statusMessages] + if (!config) return // Don't send email for other statuses + + try { + await resend.emails.send({ + from: 'United Tattoo ', + to: appointment.client_email, + subject: config.subject, + html: ` +

${config.message}

+

Hi ${appointment.client_name},

+

Artist: ${appointment.artist_name}

+

Status: ${appointment.status}

+

${config.action}

+

Questions? Call us at (719) 555-1234

+ `, + }) + } catch (error) { + console.error('Status change email failed:', error) + throw error + } +} +``` + +### 1.5 Update Confirmation Page (No Auth Required) + +**File: `app/book/confirm/[id]/page.tsx`** + +Remove authentication check - make it public with just the booking ID: + +```typescript +// NO getServerSession call! +// Anyone with the link can view their confirmation + +async function getBooking(id: string) { + const db = getDB() + + const booking = await db.prepare(` + SELECT + a.*, + ar.name as artist_name, + ar.instagram_handle + FROM appointments a + JOIN artists ar ON a.artist_id = ar.id + WHERE a.id = ? + `).bind(id).first() + + return booking +} + +export default async function BookingConfirmationPage({ params }) { + const booking = await getBooking(params.id) + + if (!booking) { + notFound() + } + + // No auth check - confirmation is public! + // Security by obscurity (UUID is hard to guess) + + return ( + // ... existing confirmation page content + ) +} +``` + +--- + +## PHASE 2 & 3: Admin Dashboards (PROTECTED) + +**These stay mostly the same, but NOW:** +- βœ… Protected by middleware +- βœ… Require Nextcloud OAuth2 login +- βœ… Check user role from database + +All the code from the original plan applies here, just add middleware protection! + +--- + +## Artist Data Linking Strategy + +### How to Link D1 Artists with Nextcloud Users + +**Option 1: Email Matching (Simple - Start Here)** + +```typescript +// When admin configures calendar, we match by email +async function linkArtistToNextcloud(artistId: string) { + const db = getDB() + + // Get artist email from D1 + const artist = await db + .prepare('SELECT email FROM artists WHERE id = ?') + .bind(artistId) + .first() + + // Calendar URL pattern for this artist + const calendarUrl = `https://portal.united-tattoos.com/remote.php/dav/calendars/${artist.email}/personal/` + + // Save configuration + await db.prepare(` + INSERT INTO artist_calendars (id, artist_id, calendar_url, calendar_id) + VALUES (?, ?, ?, 'personal') + `).bind(crypto.randomUUID(), artistId, calendarUrl).run() +} +``` + +**Option 2: Add Nextcloud User ID (Robust - Later)** + +```sql +-- Migration: Add optional nextcloud_user_id +ALTER TABLE artists ADD COLUMN nextcloud_user_id TEXT; +CREATE INDEX idx_artists_nextcloud_user ON artists(nextcloud_user_id); +``` + +Then query Nextcloud API to get user ID and store it. + +### Migration Path (Non-Breaking!) + +``` +Current State: +βœ… D1 artists table exists with all data +βœ… R2 has portfolio images +βœ… Website displays artists from D1 +βœ… Booking form uses artists from D1 + +Step 1: Create Nextcloud users for each artist + - Email must match artists.email in D1 + - Add to "artist" group in Nextcloud + +Step 2: Artists log in via OAuth2 + - They use Nextcloud credentials + - System matches by email to D1 artist record + +Step 3: Admin links calendars + - Use calendar config UI + - Matches D1 artist β†’ Nextcloud calendar via email + +βœ… No data migration needed +βœ… Existing site keeps working +βœ… Just add authentication layer on top +``` + +--- + +## Environment Variables Summary + +```env +# Existing +DATABASE_URL=... +NEXTAUTH_URL=https://united-tattoos.com +NEXTAUTH_SECRET=... +AWS_ACCESS_KEY_ID=... +AWS_SECRET_ACCESS_KEY=... +AWS_BUCKET_NAME=... + +# NEW - Email +RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxxxxxxxxxx + +# NEW - Nextcloud OAuth2 +NEXTCLOUD_CLIENT_ID=PZmqmi9vxYjtyWzt7f8QZk61jtwoAaqZ5bZz6wLvYUu4lYc0PPY6cx9qcBgDh5QI +NEXTCLOUD_CLIENT_SECRET=tkf7Ytc4vQII47OhumKBl3O3p6WhiPFQBzb5DJhw7ZjmJwDE0zTGwYGwF0MJjcsm +NEXTCLOUD_BASE_URL=https://portal.united-tattoos.com + +# NEW - CalDAV (for background sync) +NEXTCLOUD_USERNAME=admin_or_service_account +NEXTCLOUD_PASSWORD=app_password +NEXTCLOUD_CALENDAR_BASE_PATH=/remote.php/dav/calendars + +# NEW - Monitoring (optional) +NEXT_PUBLIC_SENTRY_DSN=https://xxxxx@xxxxx.ingest.sentry.io/xxxxx +``` + +--- + +## Testing Strategy + +### Phase 0 Testing +- [ ] Resend email sends successfully +- [ ] DNS records verified in Cloudflare +- [ ] Nextcloud OAuth2 login works +- [ ] Middleware protects admin routes +- [ ] Sentry captures test error + +### Phase 1 Testing (No Customer Login) +- [ ] Customer can book WITHOUT logging in +- [ ] Booking form submits successfully +- [ ] Customer receives confirmation email +- [ ] Booking syncs to Nextcloud +- [ ] Confirmation page accessible via link + +### Admin Testing (With Login) +- [ ] Admin can log in via Nextcloud +- [ ] Admin sees bookings dashboard +- [ ] Admin can approve/reject bookings +- [ ] Status changes sync to Nextcloud +- [ ] Email sent on status change + +--- + +## Key Benefits of This Approach + +βœ… **No customer friction** - Book in 2 minutes without account +βœ… **Single sign-on for staff** - One password (Nextcloud) +βœ… **No artist data migration** - D1 + R2 stays as-is +βœ… **Clean separation** - Profiles vs Authentication vs Calendar +βœ… **Existing site untouched** - All current features keep working +βœ… **Secure** - Middleware protects admin routes automatically + +--- + +## Next Steps + +1. **Review this plan** - Does the two-tier auth + hybrid data model work for you? +2. **Set up Resend** - Add domain, get API key +3. **I implement Phase 0** - Foundation (OAuth2 + Email + Middleware) +4. **Test authentication** - Make sure Nextcloud login works +5. **Proceed with booking flow** - Anonymous customer bookings + +**Ready to proceed?** + diff --git a/docs/BOOKING-WORKFLOW-RISKS.md b/docs/BOOKING-WORKFLOW-RISKS.md new file mode 100644 index 000000000..6e91cf885 --- /dev/null +++ b/docs/BOOKING-WORKFLOW-RISKS.md @@ -0,0 +1,552 @@ +# Risk Assessment & Known Issues - Booking Workflow Plan + +**Document Version:** 1.0 +**Date:** January 9, 2025 +**Status:** Pre-Implementation Review + +--- + +## πŸ”΄ Critical Risks + +### 1. Race Conditions & Concurrency +**Risk Level:** HIGH - Could cause double bookings or data loss + +**Issues:** +- User books appointment while background sync is running β†’ duplicate or conflicting data +- Two admins approve same booking simultaneously β†’ status conflicts +- Nextcloud event modified during sync β†’ data inconsistency +- No database transaction handling in appointments API + +**Mitigation Required:** +- Add database transaction locks for booking creation +- Implement optimistic locking with ETags for updates +- Add conflict resolution logic with "last write wins" or manual reconciliation +- Add unique constraints to prevent duplicates + +**Missing from Plan:** Transaction handling completely absent + +--- + +### 2. Authentication & Authorization Gaps +**Risk Level:** HIGH - Security vulnerability + +**Issues:** +- Assumption that `session.user.id` exists and matches `appointments.client_id` format +- Admin role checking duplicated in every page - error-prone +- No middleware protecting admin routes - easy to miss a check +- User table schema not verified in plan + +**Mitigation Required:** +- Create authentication middleware for all admin routes +- Verify user schema has compatible `id` field +- Add comprehensive auth tests +- Use Next.js middleware for route protection + +**Missing from Plan:** No middleware implementation, schema verification + +--- + +### 3. Background Sync Reliability +**Risk Level:** HIGH - Core functionality breaks + +**Issues:** +- Worker failures are only logged - no alerts or retries +- Nextcloud down = all syncs fail with no recovery +- Network timeouts cause partial syncs +- 5-minute sync interval = 5-minute lag for critical status changes +- No queue for failed operations + +**Mitigation Required:** +- Implement retry queue with exponential backoff +- Add Cloudflare Workers monitoring/alerting +- Create health check endpoint +- Consider webhook alternative to reduce lag +- Add dead letter queue for permanent failures + +**Missing from Plan:** Retry mechanism, monitoring, alerting + +--- + +### 4. Email Notification Dependency +**Risk Level:** HIGH - User communication breaks + +**Issues:** +- Entire workflow depends on email but marked as "TODO" +- Users/artists never know about status changes without email +- SMTP configuration might not be set +- No email templates defined +- No fallback if email fails + +**Mitigation Required:** +- Implement email system BEFORE other phases +- Choose email provider (SendGrid, Postmark, AWS SES) +- Create email templates +- Add in-app notifications as backup +- Queue failed emails for retry + +**Missing from Plan:** Email is Phase 3+ but should be Phase 1 + +--- + +## 🟑 Medium Risks + +### 5. Status Detection Brittleness +**Risk Level:** MEDIUM - Incorrect status updates + +**Issues:** +- Relies on "REQUEST:" prefix - artist could manually edit title +- External calendar events could be misidentified as bookings +- ical.js might not parse STATUS field correctly +- No validation that event belongs to booking system +- Magic string "REQUEST:" is hardcoded everywhere + +**Mitigation Required:** +- Add unique identifier (UUID) in event description +- Validate event source before processing +- Add manual reconciliation UI for admins +- Move magic strings to constants +- Add event ownership verification + +**Missing from Plan:** Event validation, reconciliation UI + +--- + +### 6. CalDAV/Nextcloud Availability +**Risk Level:** MEDIUM - Degrades user experience + +**Issues:** +- Nextcloud down = slow booking submission (waits for timeout) +- CalDAV credentials could expire without notice +- Network latency makes availability checks slow (300ms debounce helps but not enough) +- Multiple calendars per artist not supported +- Calendar URL format might vary by Nextcloud version + +**Mitigation Required:** +- Add CalDAV health check endpoint +- Implement credential rotation monitoring +- Add faster timeout for availability checks (2-3 seconds max) +- Cache availability results briefly +- Test with different Nextcloud versions + +**Missing from Plan:** Health checks, caching, timeout limits + +--- + +### 7. Performance & Scalability +**Risk Level:** MEDIUM - Won't scale beyond ~50 artists + +**Issues:** +- Background worker syncs ALL artists every 5 minutes (expensive) +- Fetches 90-day event range every sync (slow with many bookings) +- No pagination on bookings DataTable (breaks with 1000+ bookings) +- Availability check fires on every form field change +- No incremental sync using sync-token + +**Mitigation Required:** +- Implement incremental sync with sync-token (CalDAV supports this) +- Add pagination to bookings table +- Limit event range to 30 days with on-demand expansion +- Implement smarter caching for availability +- Consider sync only changed calendars + +**Missing from Plan:** Incremental sync, pagination, performance testing + +--- + +### 8. Timezone Edge Cases +**Risk Level:** MEDIUM - Wrong-time bookings + +**Issues:** +- Hardcoded America/Denver prevents expansion +- Daylight Saving Time transitions not tested +- Date comparison between systems has timezone bugs potential +- User browser timezone vs server vs Nextcloud timezone +- No verification that times are displayed correctly + +**Mitigation Required:** +- Store all times in UTC internally +- Use date-fns-tz for ALL timezone operations +- Test DST transitions (spring forward, fall back) +- Add timezone to user preferences if expanding +- Display timezone clearly in UI + +**Missing from Plan:** DST testing, UTC storage verification + +--- + +### 9. Data Consistency & Integrity +**Risk Level:** MEDIUM - Data quality degrades + +**Issues:** +- ETag conflicts if event updated simultaneously +- No global unique constraint on `caldav_uid` (only per artist) +- `calendar_sync_logs` will grow unbounded +- No validation on calendar URL format +- No cascade delete handling documented + +**Mitigation Required:** +- Add global unique constraint on `caldav_uid` +- Implement log rotation (keep last 90 days) +- Validate calendar URLs with regex +- Add ETag conflict resolution +- Document cascade delete behavior + +**Missing from Plan:** Constraints, log rotation, URL validation + +--- + +## 🟒 Low Risks (Nice to Have) + +### 10. User Experience Gaps +**Issues:** +- No way to edit booking after submission +- No user-facing cancellation flow +- Confirmation page doesn't show sync status +- No booking history for users +- No real-time updates (5-min lag) + +**Mitigation:** Add these as Phase 2 features post-launch + +--- + +### 11. Admin Experience Gaps +**Issues:** +- No bulk operations in dashboard +- No manual reconciliation UI for conflicts +- No artist notification preferences +- No test connection button (only validates on save) + +**Mitigation:** Add as Phase 3 enhancements + +--- + +### 12. Testing Coverage +**Issues:** +- No automated tests (marked TODO) +- Manual checklist not integrated into CI/CD +- No load testing +- No concurrent booking tests + +**Mitigation:** Add comprehensive test suite before production + +--- + +### 13. Monitoring & Observability +**Issues:** +- No monitoring for worker failures +- Toast errors disappear on navigation +- No dashboard for sync health +- No Sentry or error tracking + +**Mitigation:** Add monitoring in Phase 4 + +--- + +### 14. Deployment & Operations +**Issues:** +- Workers cron needs separate deployment +- No staging strategy +- No migration rollback plan +- Environment variables not documented + +**Mitigation:** Create deployment runbook + +--- + +## πŸ”§ Technical Debt & Limitations + +### 15. Architecture Limitations +- Single Nextcloud credentials (no per-artist OAuth) +- One calendar per artist only +- No recurring appointments +- No multi-day appointments +- No support for artist breaks/vacations + +### 16. Code Quality Issues +- Admin role checks duplicated (should be middleware) +- Magic strings not in constants +- No API versioning +- No TypeScript strict mode mentioned + +### 17. Missing Features (Known) +- Email notifications (CRITICAL) +- Automated tests (CRITICAL) +- Background worker deployment (CRITICAL) +- Booking edit flow +- User cancellation +- Webhook support +- In-app notifications +- SMS option + +--- + +## 🚨 Showstopper Scenarios + +### Scenario 1: Nextcloud Down During Peak Hours +**Impact:** Users book but syncs fail β†’ artists don't see bookings +**Current Plan:** Fallback to DB-only +**Gap:** No retry queue when Nextcloud returns +**Required:** Implement sync queue + +### Scenario 2: Background Worker Stops +**Impact:** No Nextcloudβ†’Web sync, status changes invisible +**Current Plan:** Worker runs but no monitoring +**Gap:** No alerts if worker dies +**Required:** Health monitoring + alerting + +### Scenario 3: Double Booking +**Impact:** Two users book same slot simultaneously +**Current Plan:** Availability check before booking +**Gap:** Race condition between check and create +**Required:** Transaction locks + +### Scenario 4: Email System Down +**Impact:** Zero user/artist communication +**Current Plan:** Email marked as TODO +**Gap:** No fallback communication method +**Required:** Email + in-app notifications + +### Scenario 5: DST Transition Bug +**Impact:** Appointments booked 1 hour off +**Current Plan:** Use date-fns-tz +**Gap:** No DST testing mentioned +**Required:** DST test suite + +--- + +## πŸ“‹ Pre-Launch Checklist + +### βœ… Must-Have (Blocking) +1. [ ] Implement email notification system with templates +2. [ ] Add authentication middleware for admin routes +3. [ ] Implement retry queue for failed syncs +4. [ ] Add transaction handling to appointments API +5. [ ] Deploy and test background worker +6. [ ] Verify timezone handling with DST tests +7. [ ] Add monitoring and alerting (Cloudflare Workers analytics + Sentry) +8. [ ] Write critical path tests (booking flow, sync flow) +9. [ ] Create deployment runbook +10. [ ] Set up staging environment with test Nextcloud + +### ⚠️ Should-Have (Important) +- [ ] Rate limiting on booking endpoint +- [ ] CSRF protection verification +- [ ] Calendar URL validation with regex +- [ ] Sync log rotation (90-day retention) +- [ ] Admin reconciliation UI for conflicts +- [ ] User booking history page +- [ ] Load test background worker (100+ artists) +- [ ] Global unique constraint on caldav_uid + +### πŸ’š Nice-to-Have (Post-Launch) +- [ ] Webhook support for instant sync (eliminate 5-min lag) +- [ ] In-app real-time notifications (WebSockets) +- [ ] User edit/cancel flows +- [ ] Bulk admin operations +- [ ] Multiple calendars per artist +- [ ] SMS notification option +- [ ] Recurring appointment support + +--- + +## 🎯 Revised Implementation Order + +### Phase 0: Critical Foundation (NEW - REQUIRED FIRST) +**Duration:** 2-3 days +**Blockers:** Authentication, email, transactions + +1. Add authentication middleware to protect admin routes +2. Verify user schema matches `appointments.client_id` +3. Add transaction handling to appointments API +4. Choose and set up email provider (SendGrid recommended) +5. Create basic email templates +6. Add error tracking (Sentry) + +**Acceptance Criteria:** +- Admin routes redirect unauthorized users +- Email sends successfully in dev +- Transaction prevents double bookings +- Errors logged to Sentry + +--- + +### Phase 1: Core Booking Flow βœ… (As Planned) +**Duration:** 3-4 days +**Dependencies:** Phase 0 complete + +1. Booking form submission with React Query +2. Confirmation page with timezone display +3. CalDAV sync on booking creation +4. Email notification on booking submission + +**Acceptance Criteria:** +- User can submit booking +- Booking appears in Nextcloud with REQUEST: prefix +- User receives confirmation email +- Toast shows success/error + +--- + +### Phase 2: Admin Infrastructure βœ… (As Planned) +**Duration:** 3-4 days +**Dependencies:** Phase 1 complete + +1. Calendar configuration UI +2. Bookings DataTable with filters +3. Approve/reject actions +4. Status sync to Nextcloud + +**Acceptance Criteria:** +- Admin can link calendars +- Admin sees pending bookings +- Approve updates status + Nextcloud +- Email sent on status change + +--- + +### Phase 3: Background Sync ⚠️ (Enhanced) +**Duration:** 4-5 days +**Dependencies:** Phase 2 complete + +1. Smart status detection logic +2. Background worker implementation +3. **NEW:** Retry queue for failed syncs +4. **NEW:** Health check endpoint +5. **NEW:** Cloudflare Workers monitoring + +**Acceptance Criteria:** +- Worker runs every 5 minutes +- Status changes detected from Nextcloud +- Failed syncs retry 3 times +- Alerts sent on persistent failures +- Health check returns sync status + +--- + +### Phase 4: Production Hardening (NEW - CRITICAL) +**Duration:** 3-4 days +**Dependencies:** Phase 3 complete + +1. Comprehensive error handling +2. Rate limiting (10 bookings/user/hour) +3. DST timezone testing +4. Load testing (100 artists, 1000 bookings) +5. Monitoring dashboard +6. Sync log rotation +7. Admin reconciliation UI + +**Acceptance Criteria:** +- All errors handled gracefully +- Rate limits prevent abuse +- DST transitions work correctly +- Worker handles load without issues +- Admins can see sync health +- Logs don't grow unbounded + +--- + +### Phase 5: Staging & Launch πŸš€ +**Duration:** 2-3 days +**Dependencies:** Phase 4 complete + +1. Deploy to staging with test Nextcloud +2. Run full test suite +3. Load test in staging +4. Security review +5. Deploy to production +6. Monitor for 48 hours + +**Acceptance Criteria:** +- All tests pass in staging +- No critical errors in 24h staging run +- Security review approved +- Production deploy successful +- Zero critical issues in first 48h + +--- + +## πŸ’‘ Recommendations + +### Before Starting Implementation + +**Critical Decisions Needed:** +1. βœ… Which email provider? (Recommend: SendGrid or Postmark) +2. βœ… Confirm user schema structure +3. βœ… Set up staging Nextcloud instance +4. βœ… Choose error tracking (Sentry vs Cloudflare Logs) +5. βœ… Define rate limits for bookings + +**Infrastructure Setup:** +1. Create staging environment +2. Set up Nextcloud test instance +3. Configure email provider +4. Set up error tracking +5. Document all environment variables + +--- + +### During Implementation + +**Code Quality:** +1. Add TypeScript strict mode +2. Create constants file for magic strings +3. Write tests alongside features +4. Add comprehensive JSDoc comments +5. Use auth middleware everywhere + +**Testing Strategy:** +1. Unit tests for sync logic +2. Integration tests for booking flow +3. E2E tests for critical paths +4. Load tests for background worker +5. DST timezone tests + +--- + +### After Implementation + +**Operations:** +1. Create runbook for common issues +2. Train staff on admin dashboards +3. Set up monitoring alerts (PagerDuty/Slack) +4. Document troubleshooting steps +5. Plan for scaling (if needed) + +**Monitoring:** +1. Track booking success rate (target: >99%) +2. Track sync success rate (target: >95%) +3. Track email delivery rate (target: >98%) +4. Monitor worker execution time (target: <30s) +5. Alert on 3 consecutive sync failures + +--- + +## πŸ“Š Risk Summary + +| Category | Critical | Medium | Low | Total | +|----------|----------|--------|-----|-------| +| Bugs/Issues | 4 | 5 | 5 | 14 | +| Missing Features | 3 | 2 | 8 | 13 | +| Technical Debt | 2 | 3 | 5 | 10 | +| **TOTAL** | **9** | **10** | **18** | **37** | + +**Showstoppers:** 5 scenarios requiring mitigation +**Blocking Issues:** 9 must-fix before production +**Estimated Additional Work:** 8-10 days (new Phase 0 + Phase 4) + +--- + +## βœ… Next Steps + +1. **Review this document with team** - Discuss acceptable risks +2. **Prioritize Phase 0 items** - Authentication + email are blocking +3. **Set up infrastructure** - Staging env, email provider, monitoring +4. **Revise timeline** - Add 8-10 days for hardening phases +5. **Get approval** - Confirm scope changes are acceptable +6. **Begin Phase 0** - Don't skip the foundation! + +--- + +**Document Status:** Ready for Review +**Requires Action:** Team discussion and approval before proceeding +