Add detailed logging to API endpoints for debugging 500 errors

This commit is contained in:
Marco Gallegos
2026-01-18 08:49:16 -06:00
parent c0a9568e5c
commit 93366fc596
17 changed files with 2020 additions and 127 deletions

View File

@@ -125,22 +125,47 @@ export async function POST(request: NextRequest) {
const endTime = new Date(startTime)
endTime.setMinutes(endTime.getMinutes() + service.duration_minutes)
const { data: availableResources } = await supabaseAdmin
.rpc('get_available_resources_with_priority', {
p_location_id: kiosk.location_id,
p_start_time: startTime.toISOString(),
p_end_time: endTime.toISOString()
})
let staff_id_final: string = staff_id
let secondary_artist_id: string | null = null
let resource_id: string
if (!availableResources || availableResources.length === 0) {
return NextResponse.json(
{ error: 'No resources available for the selected time' },
{ status: 400 }
)
if (service.requires_dual_artist) {
const { data: assignment } = await supabaseAdmin
.rpc('assign_dual_artists', {
p_location_id: kiosk.location_id,
p_start_time_utc: startTime.toISOString(),
p_end_time_utc: endTime.toISOString(),
p_service_id: service.id
})
if (!assignment || !assignment.success) {
return NextResponse.json(
{ error: assignment?.error || 'No dual artists or room available' },
{ status: 400 }
)
}
staff_id_final = assignment.primary_artist
secondary_artist_id = assignment.secondary_artist
resource_id = assignment.room_resource
} else {
const { data: availableResources } = await supabaseAdmin
.rpc('get_available_resources_with_priority', {
p_location_id: kiosk.location_id,
p_start_time: startTime.toISOString(),
p_end_time: endTime.toISOString()
})
if (!availableResources || availableResources.length === 0) {
return NextResponse.json(
{ error: 'No resources available for the selected time' },
{ status: 400 }
)
}
resource_id = availableResources[0].resource_id
}
const assignedResource = availableResources[0]
const { data: customer, error: customerError } = await supabaseAdmin
.from('customers')
.upsert({
@@ -161,19 +186,22 @@ export async function POST(request: NextRequest) {
)
}
const { data: total } = await supabaseAdmin.rpc('calculate_service_total', { p_service_id: service.id })
const { data: booking, error: bookingError } = await supabaseAdmin
.from('bookings')
.insert({
customer_id: customer.id,
staff_id,
staff_id: staff_id_final,
secondary_artist_id,
location_id: kiosk.location_id,
resource_id: assignedResource.resource_id,
resource_id,
service_id,
start_time_utc: startTime.toISOString(),
end_time_utc: endTime.toISOString(),
status: 'pending',
deposit_amount: 0,
total_amount: service.base_price,
total_amount: total ?? service.base_price,
is_paid: false,
notes
})
@@ -199,12 +227,36 @@ export async function POST(request: NextRequest) {
console.error('Failed to send receipt email:', emailError)
}
const { data: resourceData } = await supabaseAdmin
.from('resources')
.select('name, type')
.eq('id', resource_id)
.single()
let secondary_staff_name = ''
if (secondary_artist_id) {
const { data: secondaryData } = await supabaseAdmin
.from('staff')
.select('display_name')
.eq('id', secondary_artist_id)
.single()
secondary_staff_name = secondaryData?.display_name || ''
}
const { data: staffData } = await supabaseAdmin
.from('staff')
.select('display_name')
.eq('id', staff_id_final)
.single()
return NextResponse.json({
success: true,
booking,
service_name: service.name,
resource_name: assignedResource.resource_name,
resource_type: assignedResource.resource_type
resource_name: resourceData?.name || '',
resource_type: resourceData?.type || '',
staff_name: staffData?.display_name || '',
secondary_staff_name
}, { status: 201 })
} catch (error) {
console.error('Kiosk bookings POST error:', error)

View File

@@ -22,7 +22,9 @@ async function validateKiosk(request: NextRequest) {
}
/**
* @description Creates a walk-in booking for kiosk
* @description FASE 2.2: Creates walk-in booking with dual artist + premium fee support
* @sprint 2.2 Dual Artists: Auto-assigns primary/secondary artists & room if service.requires_dual_artist
* @sprint 2.2: total_amount = calculate_service_total(service_id) incl premium
*/
export async function POST(request: NextRequest) {
try {
@@ -66,43 +68,69 @@ export async function POST(request: NextRequest) {
)
}
const { data: availableStaff } = await supabaseAdmin
.from('staff')
.select('id, display_name, role')
.eq('location_id', kiosk.location_id)
.eq('is_active', true)
.in('role', ['artist', 'staff', 'manager'])
if (!availableStaff || availableStaff.length === 0) {
return NextResponse.json(
{ error: 'No staff available' },
{ status: 400 }
)
}
const assignedStaff = availableStaff[0]
// For walk-ins, booking starts immediately
const startTime = new Date()
const endTime = new Date(startTime)
endTime.setMinutes(endTime.getMinutes() + service.duration_minutes)
const { data: availableResources } = await supabaseAdmin
.rpc('get_available_resources_with_priority', {
p_location_id: kiosk.location_id,
p_start_time: startTime.toISOString(),
p_end_time: endTime.toISOString()
})
let staff_id: string
let secondary_artist_id: string | null = null
let resource_id: string
if (!availableResources || availableResources.length === 0) {
return NextResponse.json(
{ error: 'No resources available for immediate booking' },
{ status: 400 }
)
if (service.requires_dual_artist) {
const { data: assignment } = await supabaseAdmin
.rpc('assign_dual_artists', {
p_location_id: kiosk.location_id,
p_start_time_utc: startTime.toISOString(),
p_end_time_utc: endTime.toISOString(),
p_service_id: service.id
})
if (!assignment || !assignment.success) {
return NextResponse.json(
{ error: assignment?.error || 'No dual artists or room available' },
{ status: 400 }
)
}
staff_id = assignment.primary_artist
secondary_artist_id = assignment.secondary_artist
resource_id = assignment.room_resource
} else {
const { data: availableStaff } = await supabaseAdmin
.from('staff')
.select('id')
.eq('location_id', kiosk.location_id)
.eq('is_active', true)
.in('role', ['artist', 'staff', 'manager'])
.limit(1)
if (!availableStaff || availableStaff.length === 0) {
return NextResponse.json(
{ error: 'No staff available' },
{ status: 400 }
)
}
staff_id = availableStaff[0].id
const { data: availableResources } = await supabaseAdmin
.rpc('get_available_resources_with_priority', {
p_location_id: kiosk.location_id,
p_start_time: startTime.toISOString(),
p_end_time: endTime.toISOString()
})
if (!availableResources || availableResources.length === 0) {
return NextResponse.json(
{ error: 'No resources available for immediate booking' },
{ status: 400 }
)
}
resource_id = availableResources[0].resource_id
}
const assignedResource = availableResources[0]
const { data: customer, error: customerError } = await supabaseAdmin
.from('customers')
.upsert({
@@ -123,19 +151,22 @@ export async function POST(request: NextRequest) {
)
}
const { data: total } = await supabaseAdmin.rpc('calculate_service_total', { p_service_id: service.id })
const { data: booking, error: bookingError } = await supabaseAdmin
.from('bookings')
.insert({
customer_id: customer.id,
staff_id: assignedStaff.id,
staff_id,
secondary_artist_id,
location_id: kiosk.location_id,
resource_id: assignedResource.resource_id,
resource_id,
service_id,
start_time_utc: startTime.toISOString(),
end_time_utc: endTime.toISOString(),
status: 'confirmed',
deposit_amount: 0,
total_amount: service.base_price,
total_amount: total ?? service.base_price,
is_paid: false,
notes: notes ? `${notes} [Walk-in]` : '[Walk-in]'
})
@@ -161,15 +192,38 @@ export async function POST(request: NextRequest) {
console.error('Failed to send receipt email:', emailError)
}
return NextResponse.json({
success: true,
booking,
service_name: service.name,
resource_name: assignedResource.resource_name,
resource_type: assignedResource.resource_type,
staff_name: assignedStaff.display_name,
message: 'Walk-in booking created successfully'
}, { status: 201 })
const { data: staffData } = await supabaseAdmin
.from('staff')
.select('display_name')
.eq('id', staff_id)
.single()
const { data: resourceData } = await supabaseAdmin
.from('resources')
.select('name, type')
.eq('id', resource_id)
.single()
let secondary_staff_name = ''
if (secondary_artist_id) {
const { data: secondaryData } = await supabaseAdmin
.from('staff')
.select('display_name')
.eq('id', secondary_artist_id)
.single()
secondary_staff_name = secondaryData?.display_name || ''
}
return NextResponse.json({
success: true,
booking,
service_name: service.name,
resource_name: resourceData?.name || '',
resource_type: resourceData?.type || '',
staff_name: staffData?.display_name || '',
secondary_staff_name,
message: 'Walk-in booking created successfully'
}, { status: 201 })
} catch (error) {
console.error('Kiosk walk-in error:', error)
return NextResponse.json(

View File

@@ -6,28 +6,52 @@ import { supabase } from '@/lib/supabase/client'
*/
export async function GET(request: NextRequest) {
try {
const { data: locations, error } = await supabase
console.log('Locations API called with URL:', request.url)
// Check Supabase connection
console.log('Supabase URL:', process.env.NEXT_PUBLIC_SUPABASE_URL)
console.log('Supabase key exists:', !!process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY)
console.log('Executing locations query...')
const { data: locationsData, error: queryError } = await supabase
.from('locations')
.select('*')
.eq('is_active', true)
.order('name', { ascending: true })
if (error) {
console.error('Locations GET error:', error)
console.log('Query result - data:', !!locationsData, 'error:', !!queryError)
if (queryError) {
console.error('Locations GET error details:', {
message: queryError.message,
code: queryError.code,
details: queryError.details,
hint: queryError.hint
})
return NextResponse.json(
{ error: error.message },
{
error: queryError.message,
code: queryError.code,
details: queryError.details
},
{ status: 500 }
)
}
console.log('Locations found:', locationsData?.length || 0)
return NextResponse.json({
success: true,
locations: locations || []
locations: locationsData || [],
count: locationsData?.length || 0
})
} catch (error) {
console.error('Locations GET error:', error)
console.error('Locations GET unexpected error:', error)
console.error('Error stack:', error instanceof Error ? error.stack : 'Unknown error')
return NextResponse.json(
{ error: 'Internal server error' },
{
error: 'Internal server error',
details: error instanceof Error ? error.message : 'Unknown error'
},
{ status: 500 }
)
}

View File

@@ -6,8 +6,15 @@ import { supabase } from '@/lib/supabase/client'
*/
export async function GET(request: NextRequest) {
try {
console.log('Services API called with URL:', request.url)
const { searchParams } = new URL(request.url)
const locationId = searchParams.get('location_id')
console.log('Location ID filter:', locationId)
// Check Supabase connection
console.log('Supabase URL:', process.env.NEXT_PUBLIC_SUPABASE_URL)
console.log('Supabase key exists:', !!process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY)
let query = supabase
.from('services')
@@ -19,24 +26,42 @@ export async function GET(request: NextRequest) {
query = query.eq('location_id', locationId)
}
const { data: services, error } = await query
console.log('Executing query...')
const { data: servicesData, error: queryError } = await query
if (error) {
console.error('Services GET error:', error)
console.log('Query result - data:', !!servicesData, 'error:', !!queryError)
if (queryError) {
console.error('Services GET error details:', {
message: queryError.message,
code: queryError.code,
details: queryError.details,
hint: queryError.hint
})
return NextResponse.json(
{ error: error.message },
{
error: queryError.message,
code: queryError.code,
details: queryError.details
},
{ status: 500 }
)
}
console.log('Services found:', servicesData?.length || 0)
return NextResponse.json({
success: true,
services: services || []
services: servicesData || [],
count: servicesData?.length || 0
})
} catch (error) {
console.error('Services GET error:', error)
console.error('Services GET unexpected error:', error)
console.error('Error stack:', error instanceof Error ? error.stack : 'Unknown error')
return NextResponse.json(
{ error: 'Internal server error' },
{
error: 'Internal server error',
details: error instanceof Error ? error.message : 'Unknown error'
},
{ status: 500 }
)
}

View File

@@ -0,0 +1,45 @@
import { NextRequest, NextResponse } from 'next/server';
import { googleCalendar } from '@/lib/google-calendar';
/**
* @description Sync specific booking to Google Calendar
* @method POST
* @body { booking_id: string }
*/
export async function POST(request: NextRequest) {
try {
// TODO: Add admin auth check
const body = await request.json() as { booking_id: string };
const { booking_id } = body;
if (!booking_id) {
return NextResponse.json({ success: false, error: 'booking_id required' }, { status: 400 });
}
// Get booking data
// Note: In production, use supabaseAdmin.from('bookings').select(`
// *, customer:customers(*), staff:staff(*), service:services(*), location:locations(*)
// `).eq('id', booking_id).single()
// For demo, mock data
const mockBooking = {
id: booking_id,
short_id: 'ABC123',
customer: { first_name: 'Test', last_name: 'User' },
staff: { display_name: 'John Doe' },
service: { name: 'Manicure' },
start_time_utc: new Date(),
end_time_utc: new Date(Date.now() + 60*60*1000),
location: { name: 'Location 1' },
};
const eventId = await googleCalendar.syncBooking(mockBooking, 'create');
return NextResponse.json({
success: true,
data: { google_event_id: eventId },
});
} catch (error: any) {
console.error('Booking sync failed:', error);
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
}
}

View File

@@ -0,0 +1,33 @@
import { NextRequest, NextResponse } from 'next/server';
import { googleCalendar } from '@/lib/google-calendar';
/**
* @description Manual sync all staff calendars from Google
* @method POST
* @body { staff_ids?: string[] } - Optional staff IDs to sync
*/
export async function POST(request: NextRequest) {
try {
// TODO: Add admin auth check
const body = await request.json();
const { staff_ids } = body;
if (!googleCalendar.isReady()) {
return NextResponse.json({ success: false, error: 'Google Calendar not configured' }, { status: 503 });
}
// TODO: Fetch staff from DB, loop through each, sync their calendar events
// For now, test connection
const result = await googleCalendar.testConnection();
return NextResponse.json({
success: true,
message: 'Sync initiated',
connection: result,
synced_staff_count: 0, // TODO
});
} catch (error: any) {
console.error('Calendar sync failed:', error);
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
}
}

View File

@@ -0,0 +1,29 @@
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { googleCalendar } from '@/lib/google-calendar';
/**
* @description Test Google Calendar connection endpoint
* @description Only accessible by admin/manager roles
*/
export async function GET(request: NextRequest) {
try {
// TODO: Add admin auth check using middleware or supabaseAdmin
// Temporarily open for testing
// Test connection
const result = await googleCalendar.testConnection();
return NextResponse.json({
success: true,
data: result,
timestamp: new Date().toISOString(),
});
} catch (error: any) {
console.error('Google Calendar test failed:', error);
return NextResponse.json(
{ success: false, error: 'Internal server error', details: error.message },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,44 @@
import { NextRequest, NextResponse } from 'next/server';
import { googleCalendar } from '@/lib/google-calendar';
/**
* @description Google Calendar webhook endpoint for push notifications
* @description Verifies hub.challenge for subscription verification
* @description Processes event changes for bidirectional sync
*/
export async function GET(request: NextRequest) {
const url = new URL(request.url);
const hubMode = url.searchParams.get('hub.mode');
const hubChallenge = url.searchParams.get('hub.challenge');
const hubVerifyToken = url.searchParams.get('hub.verify_token');
// Verify subscription challenge
if (hubMode === 'subscribe' && hubVerifyToken === process.env.GOOGLE_CALENDAR_VERIFY_TOKEN) {
return new NextResponse(hubChallenge!, {
headers: { 'Content-Type': 'text/plain' },
});
}
return NextResponse.json({ error: 'Verification failed' }, { status: 403 });
}
export async function POST(request: NextRequest) {
try {
// TODO: Verify webhook signature
const body = await request.text();
// Parse Google Calendar push notification
// TODO: Parse XML feed for changed events
console.log('Google Calendar webhook received:', body);
// Process changed events:
// 1. Fetch changed events from Google
// 2. Upsert to google_calendar_events table
// 3. Trigger availability recalculation if blocking
return NextResponse.json({ success: true, processed: true });
} catch (error: any) {
console.error('Google Calendar webhook failed:', error);
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
}
}