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
This commit is contained in:
parent
c617934a54
commit
91afbd24f8
@ -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({
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<FeatureFlagsProvider value={initialFlags}>
|
||||
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem={false}>
|
||||
<ConstructionBanner />
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<LenisProvider>
|
||||
{children}
|
||||
{/* Global construction banner */}
|
||||
<ConstructionBanner />
|
||||
{/* Push fixed nav down when banner visible */}
|
||||
<style>{`html.has-site-banner nav.fixed{top:var(--site-banner-height,0)!important}`}</style>
|
||||
{/* Offset page content by banner height */}
|
||||
<div style={{ paddingTop: "var(--site-banner-height, 0px)" }}>{children}</div>
|
||||
<Toaster />
|
||||
</LenisProvider>
|
||||
</Suspense>
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -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<HTMLDivElement | null>(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 (
|
||||
<div className="construction-banner fixed top-0 left-0 right-0 bg-amber-500/10 border-b border-amber-500/20 backdrop-blur-sm z-[60]">
|
||||
<div className="container mx-auto px-4 h-full flex items-center">
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-2 sm:gap-4 text-amber-200/90 w-full">
|
||||
<div className="flex items-center gap-2 text-center sm:text-left">
|
||||
<AlertTriangle className="h-5 w-5 text-amber-400 flex-shrink-0" />
|
||||
<p className="text-sm font-medium">
|
||||
Website Under Construction
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-amber-300/70">•</span>
|
||||
<p className="text-sm">
|
||||
For bookings, please call:
|
||||
</p>
|
||||
<a
|
||||
href="tel:7196989004"
|
||||
className="flex items-center gap-1.5 text-sm font-semibold text-amber-300 hover:text-amber-200 transition-colors underline decoration-amber-400/30 hover:decoration-amber-300/50 underline-offset-2"
|
||||
>
|
||||
<Phone className="h-4 w-4" />
|
||||
(719) 698-9004
|
||||
</a>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleDismiss}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 sm:relative sm:right-auto sm:top-auto sm:translate-y-0 h-8 w-8 p-0 text-amber-400/70 hover:text-amber-300 hover:bg-amber-500/10"
|
||||
aria-label="Dismiss banner"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
ref={bannerRef}
|
||||
className="fixed top-0 left-0 right-0 z-[60] bg-amber-500 text-black border-b border-amber-600"
|
||||
role="region"
|
||||
aria-label="Site announcement"
|
||||
>
|
||||
<div className="relative max-w-[1800px] mx-auto px-4 lg:px-6 py-2 text-center text-sm">
|
||||
<span className="font-semibold">🚧 Site Under Construction.</span>{" "}
|
||||
For bookings, call {" "}
|
||||
<a href="tel:17196989004" className="font-semibold underline">
|
||||
719-698-9004
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
try {
|
||||
sessionStorage.setItem("constructionBannerDismissed", "1")
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
setIsVisible(false)
|
||||
}}
|
||||
aria-label="Dismiss announcement"
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 rounded p-1 text-black/80 hover:text-black hover:bg-amber-400/70 transition-colors"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
1144
docs/BOOKING-WORKFLOW-FINAL-PLAN.md
Normal file
1144
docs/BOOKING-WORKFLOW-FINAL-PLAN.md
Normal file
File diff suppressed because it is too large
Load Diff
724
docs/BOOKING-WORKFLOW-REVISED-PLAN.md
Normal file
724
docs/BOOKING-WORKFLOW-REVISED-PLAN.md
Normal file
@ -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 <bookings@united-tattoos.com>',
|
||||
to: appointment.client_email,
|
||||
subject: 'Your Tattoo Booking Request - United Tattoo',
|
||||
html: `
|
||||
<h1>Booking Request Received!</h1>
|
||||
<p>Hi ${appointment.client_name},</p>
|
||||
<p>We've received your tattoo appointment request. Here are the details:</p>
|
||||
|
||||
<div style="background: #f5f5f5; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<p><strong>Artist:</strong> ${appointment.artist_name}</p>
|
||||
<p><strong>Date:</strong> ${formattedDate}</p>
|
||||
<p><strong>Time:</strong> ${formattedTime}</p>
|
||||
<p><strong>Description:</strong> ${appointment.description}</p>
|
||||
</div>
|
||||
|
||||
<h2>What's Next?</h2>
|
||||
<ol>
|
||||
<li>Your artist will review your request (usually within 24 hours)</li>
|
||||
<li>You'll receive an email when your appointment is confirmed</li>
|
||||
<li>Bring a valid ID and arrive 10 minutes early</li>
|
||||
</ol>
|
||||
|
||||
<p>Questions? Call us at (719) 555-1234 or reply to this email.</p>
|
||||
|
||||
<p>Thanks,<br>United Tattoo Team</p>
|
||||
`,
|
||||
})
|
||||
} 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 <bookings@united-tattoos.com>',
|
||||
to: appointment.client_email,
|
||||
subject: config.subject,
|
||||
html: `
|
||||
<h1>${config.message}</h1>
|
||||
<p>Hi ${appointment.client_name},</p>
|
||||
<p><strong>Artist:</strong> ${appointment.artist_name}</p>
|
||||
<p><strong>Status:</strong> ${appointment.status}</p>
|
||||
<p>${config.action}</p>
|
||||
<p>Questions? Call us at (719) 555-1234</p>
|
||||
`,
|
||||
})
|
||||
} 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?**
|
||||
|
||||
552
docs/BOOKING-WORKFLOW-RISKS.md
Normal file
552
docs/BOOKING-WORKFLOW-RISKS.md
Normal file
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user