# 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. You'll receive another email when your appointment is confirmed
  3. Once confirmed, you can pay your deposit at the shop or via a secure payment link
  4. Show up ready! Bring a valid ID and arrive 10 minutes early

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. Log in to the admin dashboard to approve or reschedule
  3. Or edit the calendar event in Nextcloud (remove "REQUEST:" to approve)

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!