diff --git a/src/services/userPointsService.ts b/src/services/userPointsService.ts index 8a9ed77..369af6b 100644 --- a/src/services/userPointsService.ts +++ b/src/services/userPointsService.ts @@ -12,183 +12,20 @@ export const POINTS = { }; /** - * Initialize or get a user in the points system + * Ensure user ID is a valid UUID */ -export async function initializeUser(userId: string, email: string) { +function ensureUuid(userId: string): string { try { - console.log('Initializing user with raw inputs:', { userId, email }); - - // Improve email handling - if we have a bad email, use userId if it looks like an email - if (email === 'unknown@example.com' && userId.includes('@')) { - console.log('Using userId as email since it looks like a valid email'); - email = userId; - } - - // Generate a clean email - never use example.com - if (email.includes('@example.com')) { - const username = email.split('@')[0]; - email = `${username}@user.wildlife.app`; - console.log('Replaced example.com email with proper domain:', email); - } - - // Convert string userId to UUID format for Supabase - // This handles the uuid-type expected by Supabase - let userIdUUID: string; - try { - // Validate that the userId is a valid UUID format - if (validateUuid(userId)) { - userIdUUID = userId; - } else { - console.log('Warning: userId is not in UUID format, generating a deterministic UUID v5'); - // Generate a deterministic UUID v5 from the string userId - userIdUUID = uuidv5(userId, UUID_NAMESPACE); - console.log(`Converted non-UUID ${userId} to UUID ${userIdUUID}`); - } - } catch (uuidError) { - console.log('Error processing UUID, generating a new one:', uuidError); - userIdUUID = uuidv5(userId, UUID_NAMESPACE); - } - - // First try to use the new RPC function to ensure the user exists - try { - console.log('Trying ensure_user_exists RPC function...'); - const { data: ensureResult, error: ensureError } = await supabaseAdmin.rpc('ensure_user_exists', { - user_id_text: userIdUUID, - email_text: email, - initial_points: 1 // Start with 1 point - }); - - if (!ensureError && ensureResult === true) { - console.log('Successfully ensured user exists via RPC'); - - // Now get the user record - const { data: userRecord, error: userError } = await supabaseAdmin - .from('user_points') - .select('*') - .eq('user_id', userIdUUID) - .single(); - - if (!userError && userRecord) { - console.log('Retrieved user record after ensuring it exists:', userRecord); - return userRecord; - } - } else { - console.log('ensure_user_exists RPC failed or returned false:', ensureError); - } - } catch (rpcError) { - console.log('Error using ensure_user_exists RPC:', rpcError); - } - - // Fall back to traditional approach - check if user already exists - console.log('Falling back to traditional user initialization approach'); - const { data: existingUser, error: selectError } = await supabaseAdmin - .from('user_points') - .select('*') - .eq('user_id', userIdUUID) - .single(); - - if (selectError && selectError.code !== 'PGRST116') { // Not found error code - console.log('Info: Non-critical error checking for existing user:', selectError.message); - } - - if (existingUser) { - console.log('User already exists in points system:', userIdUUID); - return existingUser; - } - - console.log('Creating new user in points system:', userIdUUID, email); - - // Create new user entry - using RPC function with admin client to bypass RLS - console.log(`Attempting to create user with ID: ${userIdUUID} and email: ${email}`); - - // Direct insert is more reliable than RPC in this case - console.log('Attempting direct user insert with initial points = 1'); - const { data: directInsert, error: insertError } = await supabaseAdmin - .from('user_points') - .insert({ - user_id: userIdUUID, - email: email, - total_points: 1 // Start with 1 point for the first image - }) - .select() - .single(); - - if (!insertError) { - console.log('Successfully created user via direct insert:', directInsert); - return directInsert; + // Validate that the userId is a valid UUID format + if (validateUuid(userId)) { + return userId; } else { - console.error('Direct user insert failed:', insertError.message); + // Generate a deterministic UUID v5 from the string userId + return uuidv5(userId, UUID_NAMESPACE); } - - // Try RPC as fallback if direct insert fails - console.log('Trying RPC create_user_points as fallback'); - const { data: newUser, error } = await supabaseAdmin - .rpc('create_user_points', { - user_id_param: userIdUUID, - email_param: email, - initial_points: 1 // Start with 1 point - }); - - if (error) { - console.log('Info: Could not initialize user via RPC:', error.message); - - // Final fallback - with raw SQL execution - console.log('Attempting final direct insert fallback...'); - try { - // Try raw SQL execution as a last resort - const { error: sqlError } = await supabaseAdmin.rpc('create_user_with_raw_sql', { - user_id_text: userIdUUID, - email_text: email - }); - - if (!sqlError) { - console.log('Successfully created user with raw SQL'); - - // Get the user - const { data: userAfterRaw } = await supabaseAdmin - .from('user_points') - .select('*') - .eq('user_id', userIdUUID) - .single(); - - if (userAfterRaw) { - return userAfterRaw; - } - } else { - console.log('Raw SQL user creation failed:', sqlError.message); - } - } catch (sqlError) { - console.log('Exception in raw SQL user creation:', sqlError); - } - - // Really last fallback - direct insert - console.log('Attempting direct insert as absolute last fallback...'); - const { data: directInsert, error: insertError } = await supabaseAdmin - .from('user_points') - .insert({ - user_id: userIdUUID, - email, - total_points: 1 // Start with 1 point for first image - }) - .select() - .single(); - - if (insertError) { - console.warn('Direct insert also failed:', insertError.message); - // Don't throw here, just return null and we'll handle it later - return null; - } - - return directInsert; - } - - // For RPC response, we might need to reconstruct the user object - return newUser || { user_id: userIdUUID, email, total_points: 0 }; } catch (error) { - console.warn('Note: Unexpected error in initializeUser:', error); - // Instead of throwing, we'll return a basic user object with properly formatted UUID - const fallbackUUID = validateUuid(userId) ? userId : uuidv5(userId, UUID_NAMESPACE); - return { user_id: fallbackUUID, email, total_points: 0 }; + console.log('Error processing UUID, generating a new one:', error); + return uuidv5(userId, UUID_NAMESPACE); } } @@ -204,7 +41,7 @@ export async function awardPointsForImage( animalType: string ) { try { - if (!userId || !email) { + if (!userId) { console.warn('Missing user data, cannot award points', { userId, email }); return { success: false, @@ -212,78 +49,41 @@ export async function awardPointsForImage( }; } - // Log what email and userId we're starting with console.log('Award points starting values:', { userId, email }); - // Make sure we have a valid email - NOT a generated one - if (email === 'unknown@example.com' || email.endsWith('@user.wildlife.app')) { - console.log('WARNING: Using a synthetic email address:', email); - } - - // Always prefer the email parameter directly from the authentication system - if (!email || email === 'unknown@example.com') { - if (userId.includes('@')) { - email = userId; - console.log('Using userId as email since it looks like an email:', email); - } else { - // This should be very rare - only if both auth email and userId are invalid - console.log('WARNING: Both userId and email are invalid or missing'); - } - } - // Convert userId to UUID format - let userIdUUID: string; - try { - if (validateUuid(userId)) { - userIdUUID = userId; - } else { - console.log('Warning: userId is not in UUID format in awardPointsForImage'); - // Generate a deterministic UUID v5 from the string userId - userIdUUID = uuidv5(userId, UUID_NAMESPACE); - console.log(`Converted non-UUID ${userId} to UUID ${userIdUUID}`); - } - } catch (uuidError) { - console.log('Error processing UUID in awardPointsForImage, generating one:', uuidError); - userIdUUID = uuidv5(userId, UUID_NAMESPACE); - } + const userIdUUID = ensureUuid(userId); + console.log(`Using UUID ${userIdUUID} for user ${userId}`); // Generate hash for the image const imageHash = generateImageHash(imageBuffer); console.log(`Processing points for image ${imageHash.substring(0, 8)}... for user ${userIdUUID}`); - // DIAGNOSTIC: Log all existing transactions to see what's happening - console.log('DIAGNOSTIC: Looking for all transactions in the database'); - try { - const { data: allTransactions, error: allTxError } = await supabaseAdmin - .from('point_transactions') - .select('*'); - - console.log('All transactions count:', allTransactions?.length || 0); - console.log('First few transactions:', allTransactions?.slice(0, 3)); - - if (allTxError) { - console.error('Error fetching all transactions:', allTxError); - } - } catch (diagError) { - console.error('Exception in diagnostic transaction fetch:', diagError); - } - - // Check if this image has already been awarded points with admin client - console.log('Checking for existing transaction with hash:', imageHash); - const { data: existingTransaction, error: checkError } = await supabaseAdmin - .from('point_transactions') - .select('*') - .eq('image_hash', imageHash) - .eq('user_id', userIdUUID) - .maybeSingle(); // Use maybeSingle instead of single to avoid errors + // Check if this image has already been awarded points + const { data: existingImage, error: checkError } = await supabaseAdmin.rpc('check_duplicate_upload', { + user_id_param: userIdUUID, + image_hash_param: imageHash + }); if (checkError) { - console.log('Info: Non-critical error checking transaction:', checkError.message); - } - - console.log('Existing transaction check result:', existingTransaction); - - if (existingTransaction) { + console.log('Error checking for duplicate image:', checkError.message); + + // Fallback to direct query + const { data: existingTransaction, error: directError } = await supabaseAdmin + .from('point_transactions') + .select('*') + .eq('image_hash', imageHash) + .eq('user_id', userIdUUID) + .maybeSingle(); + + if (!directError && existingTransaction) { + console.log(`Points already awarded for image ${imageHash.substring(0, 8)}...`); + return { + success: false, + message: 'Points already awarded for this image' + }; + } + } else if (existingImage) { console.log(`Points already awarded for image ${imageHash.substring(0, 8)}...`); return { success: false, @@ -291,268 +91,56 @@ export async function awardPointsForImage( }; } - // DIAGNOSTIC: Log all users to see what's happening - console.log('DIAGNOSTIC: Looking for all users in the database'); - try { - const { data: allUsers, error: allUsersError } = await supabaseAdmin - .from('user_points') - .select('*'); - - console.log('All users count:', allUsers?.length || 0); - console.log('All users:', allUsers); + // Create transaction using RPC + const { data: transactionData, error: rpcError } = await supabaseAdmin + .rpc('create_point_transaction', { + user_id_param: userIdUUID, + points_param: POINTS.IMAGE_UPLOAD, + transaction_type_param: 'image_upload', + image_hash_param: imageHash, + image_url_param: imageUrl, + description_param: description, + animal_type_param: animalType + }); - if (allUsersError) { - console.error('Error fetching all users:', allUsersError); - } - } catch (diagError) { - console.error('Exception in diagnostic users fetch:', diagError); - } - - // Make sure user exists in our system - with FORCED initialization - console.log(`Creating or getting user: ${userIdUUID} with email: ${email}`); - const user = await initializeUser(userId, email); - - if (!user) { - console.warn('Failed to initialize or find user, will create transaction without points update'); - } else { - console.log('User initialized or found:', user); - } - - console.log('Creating point transaction for user:', userIdUUID); - - // We'll try these approaches in sequence, with detailed logging - let transactionCreated = false; - - // 1. Try to create transaction using RPC first with admin client - try { - console.log('Attempting to create transaction via RPC...'); - const { data: transactionData, error: rpcError } = await supabaseAdmin - .rpc('create_point_transaction', { - user_id_param: userIdUUID, - points_param: POINTS.IMAGE_UPLOAD, - transaction_type_param: 'image_upload', - image_hash_param: imageHash, - image_url_param: imageUrl, - description_param: description, - animal_type_param: animalType - }); + if (rpcError) { + console.log('Error creating transaction via RPC:', rpcError.message); + + // Fallback to direct insert + const { data: insertData, error: insertError } = await supabaseAdmin + .from('point_transactions') + .insert({ + user_id: userIdUUID, + points: POINTS.IMAGE_UPLOAD, + transaction_type: 'image_upload', + image_hash: imageHash, + image_url: imageUrl, + description, + animal_type: animalType + }) + .select() + .single(); - if (!rpcError) { - console.log('Successfully created transaction via RPC:', transactionData); - transactionCreated = true; - } else { - console.log('Could not create transaction via RPC:', rpcError.message); - } - } catch (error) { - console.error('Exception in RPC transaction creation:', error); - } - - // 2. Fallback to direct insert with admin client - if (!transactionCreated) { - try { - console.log('Attempting direct insert for transaction...'); - const { data: insertData, error: transactionError } = await supabaseAdmin - .from('point_transactions') - .insert({ - user_id: userIdUUID, - points: POINTS.IMAGE_UPLOAD, - transaction_type: 'image_upload', - image_hash: imageHash, - image_url: imageUrl, - description, - animal_type: animalType - }) - .select() - .single(); - - if (transactionError) { - console.warn('Direct transaction insert failed:', transactionError.message); - } else { - console.log('Successfully created transaction via direct insert:', insertData); - transactionCreated = true; - } - } catch (error) { - console.error('Exception in direct transaction insert:', error); - } - } - - // 3. Try with raw SQL as last resort - if (!transactionCreated) { - try { - console.log('Attempting direct SQL insert as last resort...'); - - // Fetch the next ID in the point_transactions sequence - const { data: seqData, error: seqError } = await supabaseAdmin.rpc('get_next_transaction_id', {}); - const nextId = seqData || 1; - - if (seqError) { - console.warn('Error getting sequence ID:', seqError.message); - console.log('Trying with hard-coded transaction entry...'); - - // Direct minimal transaction insert - const { error: minimalError } = await supabaseAdmin - .from('point_transactions') - .insert({ - user_id: userIdUUID, - points: POINTS.IMAGE_UPLOAD, - transaction_type: 'image_upload', - image_hash: imageHash - }); - - if (minimalError) { - console.warn('Even minimal transaction insert failed:', minimalError.message); - // One final attempt - execute raw SQL - const { error: rawError } = await supabaseAdmin.rpc('insert_minimal_transaction', { - user_id_text: userIdUUID, - points_num: POINTS.IMAGE_UPLOAD - }); - - if (rawError) { - console.error('All transaction creation methods failed:', rawError.message); - return { - success: false, - message: 'Failed to record points transaction after all attempts' - }; - } else { - console.log('Successfully created transaction via raw SQL function'); - transactionCreated = true; - } - } else { - console.log('Successfully created minimal transaction'); - transactionCreated = true; - } - } else { - console.log('Using sequence ID for transaction:', nextId); - const { error: sqlInsertError } = await supabaseAdmin.rpc('insert_transaction_with_id', { - id_num: nextId, - user_id_text: userIdUUID, - points_num: POINTS.IMAGE_UPLOAD, - hash_text: imageHash - }); - - if (sqlInsertError) { - console.error('SQL transaction insert failed:', sqlInsertError.message); - return { - success: false, - message: 'Failed to record points transaction with SQL' - }; - } else { - console.log('Successfully created transaction via SQL function'); - transactionCreated = true; - } - } - } catch (error) { - console.error('Exception in SQL transaction insert:', error); + if (insertError) { + console.error('Failed to record transaction:', insertError.message); return { success: false, message: 'Failed to record points transaction' }; } + + console.log('Successfully created transaction via direct insert:', insertData); + } else { + console.log('Successfully created transaction via RPC:', transactionData); } - // Try to update user's total points (if we created the transaction successfully) - console.log('Updating total points for user:', userIdUUID); - - let pointsUpdated = false; - - // 1. Try RPC method first with admin client - try { - console.log('Attempting to update points via RPC...'); - const { data: rpcData, error: updateError } = await supabaseAdmin.rpc('increment_user_points', { - user_id_param: userIdUUID, - points_param: POINTS.IMAGE_UPLOAD - }); - - if (!updateError) { - console.log('Successfully updated points via RPC:', rpcData); - pointsUpdated = true; - } else { - console.log('Could not update points via RPC:', updateError.message); - } - } catch (error) { - console.error('Exception in RPC points update:', error); - } - - // 2. Fallback: Direct update with admin client - if (!pointsUpdated) { - try { - console.log('Attempting direct update for points...'); - - // Get current points first with admin client - const { data: currentPointsData, error: selectError } = await supabaseAdmin - .from('user_points') - .select('total_points') - .eq('user_id', userIdUUID) - .maybeSingle(); - - if (selectError) { - console.warn('Could not get current points:', selectError.message); - } else { - console.log('Current points data:', currentPointsData); - - const currentPoints = currentPointsData?.total_points || 0; - const newPoints = currentPoints + POINTS.IMAGE_UPLOAD; - - console.log(`Updating points from ${currentPoints} to ${newPoints}`); - - // Then update with new total with admin client - const { data: updateData, error: fallbackError } = await supabaseAdmin - .from('user_points') - .update({ - total_points: newPoints, - updated_at: new Date().toISOString() - }) - .eq('user_id', userIdUUID) - .select() - .single(); - - if (!fallbackError) { - console.log('Successfully updated points via direct update:', updateData); - pointsUpdated = true; - } else { - console.warn('Direct points update failed:', fallbackError.message); - } - } - } catch (err) { - console.error('Exception in direct points update:', err); - } - } - - // 3. Last resort: Force an update to 1 point if this is their first transaction - if (!pointsUpdated) { - try { - console.log('Attempting force update as last resort...'); - const { error: forceError } = await supabaseAdmin - .from('user_points') - .update({ - total_points: 1, // Force to at least 1 point - updated_at: new Date().toISOString() - }) - .eq('user_id', userIdUUID); - - if (!forceError) { - console.log('Successfully forced points update to 1'); - pointsUpdated = true; - } else { - console.warn('Even forced points update failed:', forceError.message); - } - } catch (err) { - console.error('Exception in forced points update:', err); - } - } - - // Return success even if points update failed but transaction was created return { success: true, points: POINTS.IMAGE_UPLOAD, - pointsUpdated, - message: pointsUpdated - ? 'Points awarded successfully' - : 'Transaction recorded but points update failed' + message: 'Points awarded successfully' }; } catch (error) { - console.warn('Note: Error in awardPointsForImage:', error); - // Don't throw, return an error response instead + console.warn('Error in awardPointsForImage:', error); return { success: false, message: 'Failed to award points due to an unexpected error' @@ -570,190 +158,66 @@ export async function getUserPoints(userId: string) { return 0; } - // If this is an email address, note it for debugging + console.log('Fetching points for user:', userId); + + // Generate potential IDs to try + const identifiersToTry = [userId]; + + // If userId is an email, add a UUID version if (userId.includes('@')) { - console.log('Note: Email address used as user ID for points:', userId); + const emailUuid = uuidv5(userId, UUID_NAMESPACE); + identifiersToTry.push(emailUuid); + } + // If it's not a UUID, generate one + else if (!validateUuid(userId)) { + const generatedUuid = uuidv5(userId, UUID_NAMESPACE); + identifiersToTry.push(generatedUuid); } - // Convert to UUID - let userIdUUID: string; - try { - if (validateUuid(userId)) { - userIdUUID = userId; - } else { - console.log('Warning: userId is not in UUID format in getUserPoints'); - // Generate a deterministic UUID v5 from the string userId - userIdUUID = uuidv5(userId, UUID_NAMESPACE); - console.log(`Converted non-UUID ${userId} to UUID ${userIdUUID}`); - } - } catch (uuidError) { - console.log('Error processing UUID in getUserPoints, generating one:', uuidError); - userIdUUID = uuidv5(userId, UUID_NAMESPACE); - } + console.log('Will try these identifiers for points calculation:', identifiersToTry); - // Check if we need to initialize the user first - try { - // This is a lightweight check - just to see if the user exists - using admin client - const { data: userExists, error: checkError } = await supabaseAdmin - .from('user_points') - .select('id') - .eq('user_id', userIdUUID) - .maybeSingle(); - - // If we got an empty result or a not found error, the user doesn't exist yet - if (!userExists || (checkError && checkError.code === 'PGRST116')) { - console.log('User does not exist yet in user_points table, need to initialize'); - - // At this point, the user should already be authenticated, so we should have their real email - // But if we don't, we need to handle that case - const email = userId.includes('@') ? userId : `${userId}@user.wildlife.app`; - - console.log('Initializing user with:', { userId, email }); - await initializeUser(userId, email); - } - } catch (initError) { - // Just log this error, don't prevent the rest of the function from executing - console.log('Note: Error checking/initializing user:', initError); - } - - // IMPROVED: ALWAYS use the direct query with the admin client for reliable results - try { - console.log('Getting points from direct query for user:', userIdUUID); + // Try getting total points using the RPC function + for (const idToTry of identifiersToTry) { + const userUuid = validateUuid(idToTry) ? idToTry : uuidv5(idToTry, UUID_NAMESPACE); - // First try an exact UUID match - let { data, error } = await supabaseAdmin - .from('user_points') - .select('total_points') - .eq('user_id', userIdUUID) - .maybeSingle(); - - console.log('Direct query result (exact match):', { data, error }); - - if (!error && data && data.total_points > 0) { - console.log('Returning total_points from direct query (exact match):', data.total_points); - return data.total_points; - } - - // If no exact match or total is 0, get ALL points records and search for a match - console.log('No exact match with non-zero points, fetching all records...'); - const { data: allData, error: allError } = await supabaseAdmin - .from('user_points') - .select('*'); - - if (!allError && allData && allData.length > 0) { - console.log('All user_points records:', allData); - - // Check each record to see if any user_id matches our ID (case insensitive) - const matchingRecord = allData.find(record => - String(record.user_id).toLowerCase() === userIdUUID.toLowerCase() - ); - - if (matchingRecord && matchingRecord.total_points > 0) { - console.log('Found matching record with points:', matchingRecord); - return matchingRecord.total_points; - } - } - - // If still no points or 0 points, calculate from transactions - console.log('No points record found or total is 0. Calculating from transactions...'); - - // Get all transactions for this user - const { data: userTransactions, error: txError } = await supabaseAdmin - .from('point_transactions') - .select('points') - .eq('user_id', userIdUUID); - - if (!txError && userTransactions && userTransactions.length > 0) { - console.log(`Found ${userTransactions.length} transactions, calculating total...`); - - // Sum up all the points from transactions - const calculatedTotal = userTransactions.reduce((total, tx) => total + (tx.points || 0), 0); - - console.log('Calculated total from transactions:', calculatedTotal); - - if (calculatedTotal > 0) { - // Update the user_points record with the correct total - const { error: updateError } = await supabaseAdmin - .from('user_points') - .update({ - total_points: calculatedTotal, - updated_at: new Date().toISOString() - }) - .eq('user_id', userIdUUID); - - if (updateError) { - console.log('Error updating total points:', updateError); - } else { - console.log('Updated user_points record with correct total:', calculatedTotal); - } - - return calculatedTotal; - } - } - - // If we still don't have data, log this but continue to the RPC method - if (error) { - console.log('Info: Could not get points via direct query:', error.message); - } - } catch (directQueryError) { - console.log('Info: Error in direct points query:', directQueryError); - } - - // Use RPC method as fallback with admin client - try { - console.log('Trying RPC get_current_points for user:', userIdUUID); - const { data: rpcData, error: rpcError } = await supabaseAdmin - .rpc('get_current_points', { - user_id_param: userIdUUID + try { + const { data: totalPoints, error } = await supabaseAdmin.rpc('get_user_total_points', { + user_id_param: userUuid }); - - console.log('RPC result:', { rpcData, rpcError }); - if (!rpcError && typeof rpcData === 'number') { - console.log('Returning points from RPC:', rpcData); - return rpcData; - } - - if (rpcError) { - console.log('Info: Could not get points via RPC:', rpcError.message); - } - } catch (rpcError) { - console.log('Info: Error in RPC points query:', rpcError); - } - - // Last resort - try a different transaction query with string comparison - try { - console.log('Last resort - trying to calculate points from all transactions'); - const { data: allTxs, error: allTxError } = await supabaseAdmin - .from('point_transactions') - .select('*'); - - if (!allTxError && allTxs && allTxs.length > 0) { - // Find transactions where user_id string matches our UUID (case insensitive) - const matchingTxs = allTxs.filter(tx => - String(tx.user_id).toLowerCase() === userIdUUID.toLowerCase() - ); - - if (matchingTxs.length > 0) { - console.log(`Found ${matchingTxs.length} matching transactions in full table scan`); - const calculatedPoints = matchingTxs.reduce((total, tx) => total + (tx.points || 0), 0); - - console.log('Calculated points from full table scan:', calculatedPoints); - if (calculatedPoints > 0) { - return calculatedPoints; - } + if (!error) { + console.log(`Total points for user ${userUuid}: ${totalPoints}`); + return totalPoints || 0; } + } catch (err) { + console.log(`Error getting points for ID ${userUuid}:`, err); } - } catch (err) { - console.log('Error in last resort points calculation:', err); } - // If we reach here, all methods failed, return 0 - console.log('All point retrieval methods failed, returning 0'); + // Fallback to direct calculation + for (const idToTry of identifiersToTry) { + const userUuid = validateUuid(idToTry) ? idToTry : uuidv5(idToTry, UUID_NAMESPACE); + + try { + const { data: transactions, error } = await supabaseAdmin + .from('point_transactions') + .select('points') + .eq('user_id', userUuid); + + if (!error && transactions && transactions.length > 0) { + const totalPoints = transactions.reduce((sum, tx) => sum + (tx.points || 0), 0); + console.log(`Calculated ${totalPoints} points from ${transactions.length} transactions`); + return totalPoints; + } + } catch (err) { + console.log(`Error calculating points for ID ${userUuid}:`, err); + } + } + + // If all methods fail, return 0 return 0; } catch (error) { - // Global catch for unexpected errors - console.log('Note: Unexpected error in getUserPoints, returning 0:', error); + console.log('Unexpected error in getUserPoints:', error); return 0; } } @@ -768,100 +232,59 @@ export async function getUserTransactions(userId: string) { return []; } - // If this is an email address, note it for consistency + console.log('Fetching transactions for user:', userId); + + // Generate potential IDs to try + const identifiersToTry = [userId]; + + // If userId is an email, add a UUID version if (userId.includes('@')) { - console.log('Note: Email address used as user ID for transactions:', userId); + const emailUuid = uuidv5(userId, UUID_NAMESPACE); + identifiersToTry.push(emailUuid); + } + // If it's not a UUID, generate one + else if (!validateUuid(userId)) { + const generatedUuid = uuidv5(userId, UUID_NAMESPACE); + identifiersToTry.push(generatedUuid); } - // Convert to UUID - let userIdUUID: string; - try { - if (validateUuid(userId)) { - userIdUUID = userId; - } else { - console.log('Warning: userId is not in UUID format in getUserTransactions'); - // Generate a deterministic UUID v5 from the string userId - userIdUUID = uuidv5(userId, UUID_NAMESPACE); - console.log(`Converted non-UUID ${userId} to UUID ${userIdUUID}`); - } - } catch (uuidError) { - console.log('Error processing UUID in getUserTransactions, generating one:', uuidError); - userIdUUID = uuidv5(userId, UUID_NAMESPACE); - } + console.log('Will try these identifiers for transaction lookup:', identifiersToTry); - // IMPROVED: Better transaction retrieval - try { - console.log('Getting transactions from direct query for user:', userIdUUID); - // First try direct exact match - const { data, error } = await supabaseAdmin - .from('point_transactions') - .select('*') - .eq('user_id', userIdUUID) - .order('created_at', { ascending: false }); - - console.log('Direct transaction query result:', { count: data?.length, error }); - - if (!error && Array.isArray(data) && data.length > 0) { - console.log('Returning transactions from direct query:', data.length); - return data; - } + // Try each identifier + for (const idToTry of identifiersToTry) { + const userUuid = validateUuid(idToTry) ? idToTry : uuidv5(idToTry, UUID_NAMESPACE); - // If no exact match, get ALL transactions and search for a match - console.log('No exact match for transactions, fetching all records...'); - const { data: allData, error: allError } = await supabaseAdmin - .from('point_transactions') - .select('*') - .order('created_at', { ascending: false }); - - if (!allError && allData && allData.length > 0) { - console.log('All transactions (first 5):', allData.slice(0, 5)); - - // Check each record to see if any user_id matches our ID (case insensitive) - const matchingRecords = allData.filter(record => - String(record.user_id).toLowerCase() === userIdUUID.toLowerCase() - ); - - if (matchingRecords.length > 0) { - console.log('Found matching transactions:', matchingRecords.length); - return matchingRecords; - } - } - - // Log but continue to the RPC method - if (error) { - console.log('Info: Could not get transactions via direct query:', error.message); - } - } catch (directQueryError) { - console.log('Info: Error in direct transactions query:', directQueryError); - } - - // Try RPC method as fallback with admin client - try { - console.log('Trying RPC get_user_transactions for user:', userIdUUID); - const { data: rpcData, error: rpcError } = await supabaseAdmin - .rpc('get_user_transactions', { - user_id_param: userIdUUID + try { + // Try the RPC function first + const { data: transactions, error: rpcError } = await supabaseAdmin.rpc('get_user_transactions', { + user_id_param: userUuid }); - console.log('RPC transactions result:', { count: rpcData?.length, rpcError }); + if (!rpcError && transactions && transactions.length > 0) { + console.log(`Found ${transactions.length} transactions for ${userUuid} via RPC`); + return transactions; + } - if (!rpcError && Array.isArray(rpcData)) { - console.log('Returning transactions from RPC:', rpcData.length); - return rpcData; + // Fallback to direct query + const { data: directTransactions, error } = await supabaseAdmin + .from('point_transactions') + .select('*') + .eq('user_id', userUuid) + .order('created_at', { ascending: false }); + + if (!error && directTransactions && directTransactions.length > 0) { + console.log(`Found ${directTransactions.length} transactions for ${userUuid} via direct query`); + return directTransactions; + } + } catch (err) { + console.log(`Error getting transactions for ID ${userUuid}:`, err); } - - if (rpcError) { - console.log('Info: Could not get transactions via RPC:', rpcError.message); - } - } catch (rpcError) { - console.log('Info: Error in RPC transactions query:', rpcError); } - - // If we reach here, return empty array as fallback + + // If no transactions found, return empty array return []; } catch (error) { - // Global catch for all unexpected errors - console.log('Note: Unexpected error in getUserTransactions, returning empty array:', error); + console.log('Unexpected error in getUserTransactions:', error); return []; } } \ No newline at end of file diff --git a/supabase-drop-functions.sql b/supabase-drop-functions.sql index 3524b78..37f05d9 100644 --- a/supabase-drop-functions.sql +++ b/supabase-drop-functions.sql @@ -1,6 +1,22 @@ --- Drop existing functions to avoid conflicts +-- Drop existing functions from the previous schema to avoid conflicts +-- This file should be run before applying the new schema + +-- Drop functions from the user_points system DROP FUNCTION IF EXISTS increment_user_points; DROP FUNCTION IF EXISTS get_current_points; DROP FUNCTION IF EXISTS create_user_points; DROP FUNCTION IF EXISTS create_point_transaction; -DROP FUNCTION IF EXISTS get_user_transactions; \ No newline at end of file +DROP FUNCTION IF EXISTS get_user_transactions; + +-- Drop functions from schema updates +DROP FUNCTION IF EXISTS get_next_transaction_id; +DROP FUNCTION IF EXISTS insert_transaction_with_id; +DROP FUNCTION IF EXISTS insert_minimal_transaction; +DROP FUNCTION IF EXISTS ensure_user_exists; +DROP FUNCTION IF EXISTS get_user_points_by_id; +DROP FUNCTION IF EXISTS force_set_user_points; +DROP FUNCTION IF EXISTS create_user_with_raw_sql; + +-- The following command can be used to drop the user_points table after migrating all data +-- This is commented out as it should be run manually after ensuring successful data migration +-- DROP TABLE IF EXISTS user_points; \ No newline at end of file diff --git a/supabase-schema-updates.sql b/supabase-schema-updates.sql index c3027a1..353c9d0 100644 --- a/supabase-schema-updates.sql +++ b/supabase-schema-updates.sql @@ -1,152 +1,69 @@ --- Additional SQL functions to improve reliability of user points system +-- Additional SQL functions for the simplified points system +-- This file is meant to be run after supabase-schema.sql --- Function to get the next transaction ID -CREATE OR REPLACE FUNCTION get_next_transaction_id() -RETURNS BIGINT AS $$ -BEGIN - RETURN nextval('point_transactions_id_seq'); -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - --- Function to insert a transaction with a specific ID -CREATE OR REPLACE FUNCTION insert_transaction_with_id( - id_num BIGINT, - user_id_text TEXT, - points_num INTEGER, - hash_text TEXT DEFAULT NULL -) -RETURNS BOOLEAN AS $$ -BEGIN - INSERT INTO point_transactions ( - id, - user_id, - points, - transaction_type, - image_hash, - created_at - ) - VALUES ( - id_num, - user_id_text::UUID, - points_num, - 'image_upload', - hash_text, - now() - ); - - RETURN TRUE; -EXCEPTION WHEN OTHERS THEN - RAISE NOTICE 'Error in insert_transaction_with_id: %', SQLERRM; - RETURN FALSE; -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - --- Minimal transaction insert function -CREATE OR REPLACE FUNCTION insert_minimal_transaction( - user_id_text TEXT, - points_num INTEGER -) -RETURNS BOOLEAN AS $$ -BEGIN - INSERT INTO point_transactions ( - user_id, - points, - transaction_type, - created_at - ) - VALUES ( - user_id_text::UUID, - points_num, - 'image_upload', - now() - ); - - RETURN TRUE; -EXCEPTION WHEN OTHERS THEN - RAISE NOTICE 'Error in insert_minimal_transaction: %', SQLERRM; - RETURN FALSE; -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - --- Function to ensure a user exists, creating if needed -CREATE OR REPLACE FUNCTION ensure_user_exists( - user_id_text TEXT, - email_text TEXT, - initial_points INTEGER DEFAULT 0 +-- Function to check duplicate image uploads for a specific user +CREATE OR REPLACE FUNCTION check_duplicate_upload( + user_id_param TEXT, + image_hash_param TEXT ) RETURNS BOOLEAN AS $$ DECLARE - user_exists BOOLEAN; + exists_flag BOOLEAN; BEGIN - -- Check if user exists SELECT EXISTS ( - SELECT 1 FROM user_points WHERE user_id = user_id_text::UUID - ) INTO user_exists; + SELECT 1 FROM point_transactions + WHERE user_id = user_id_param::UUID + AND image_hash = image_hash_param + ) INTO exists_flag; - -- If user doesn't exist, create them - IF NOT user_exists THEN - INSERT INTO user_points ( - user_id, - email, - total_points, - created_at, - updated_at - ) - VALUES ( - user_id_text::UUID, - email_text, - initial_points, - now(), - now() - ); - END IF; - - RETURN TRUE; -EXCEPTION WHEN OTHERS THEN - RAISE NOTICE 'Error in ensure_user_exists: %', SQLERRM; - RETURN FALSE; + RETURN exists_flag; END; $$ LANGUAGE plpgsql SECURITY DEFINER; --- Function to force set user points to a specific value -CREATE OR REPLACE FUNCTION force_set_user_points( - user_id_text TEXT, - points_value INTEGER +-- Function to get transaction count for a user +CREATE OR REPLACE FUNCTION get_user_transaction_count( + user_id_param TEXT ) -RETURNS BOOLEAN AS $$ +RETURNS INTEGER AS $$ +DECLARE + count_result INTEGER; BEGIN - UPDATE user_points - SET - total_points = points_value, - updated_at = now() - WHERE user_id = user_id_text::UUID; + SELECT COUNT(*) INTO count_result + FROM point_transactions + WHERE user_id = user_id_param::UUID; - RETURN FOUND; -EXCEPTION WHEN OTHERS THEN - RAISE NOTICE 'Error in force_set_user_points: %', SQLERRM; - RETURN FALSE; + RETURN count_result; END; $$ LANGUAGE plpgsql SECURITY DEFINER; --- Function to create a user with raw SQL -CREATE OR REPLACE FUNCTION create_user_with_raw_sql( - user_id_text TEXT, - email_text TEXT +-- Function to get a user's first transaction date +CREATE OR REPLACE FUNCTION get_user_first_transaction_date( + user_id_param TEXT ) -RETURNS BOOLEAN AS $$ +RETURNS TIMESTAMP WITH TIME ZONE AS $$ +DECLARE + first_date TIMESTAMP WITH TIME ZONE; BEGIN - -- Directly execute the SQL insert - EXECUTE format(' - INSERT INTO user_points (user_id, email, total_points, created_at, updated_at) - VALUES (%L::UUID, %L, 1, now(), now()) - ON CONFLICT (user_id) DO UPDATE - SET updated_at = now() - RETURNING id - ', user_id_text, email_text); + SELECT MIN(created_at) INTO first_date + FROM point_transactions + WHERE user_id = user_id_param::UUID; - RETURN TRUE; -EXCEPTION WHEN OTHERS THEN - RAISE NOTICE 'Error in create_user_with_raw_sql: %', SQLERRM; - RETURN FALSE; + RETURN first_date; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- Function to get a user's latest transaction date +CREATE OR REPLACE FUNCTION get_user_latest_transaction_date( + user_id_param TEXT +) +RETURNS TIMESTAMP WITH TIME ZONE AS $$ +DECLARE + latest_date TIMESTAMP WITH TIME ZONE; +BEGIN + SELECT MAX(created_at) INTO latest_date + FROM point_transactions + WHERE user_id = user_id_param::UUID; + + RETURN latest_date; END; $$ LANGUAGE plpgsql SECURITY DEFINER; \ No newline at end of file diff --git a/supabase-schema.sql b/supabase-schema.sql index 585fdd4..3c8f281 100644 --- a/supabase-schema.sql +++ b/supabase-schema.sql @@ -1,20 +1,10 @@ -- Schema for Supabase to set up user points tracking --- This file is just for reference and should be executed in the Supabase SQL Editor - --- Create user_points table to track total points for each user -CREATE TABLE IF NOT EXISTS user_points ( - id BIGSERIAL PRIMARY KEY, - user_id UUID NOT NULL UNIQUE, - email TEXT NOT NULL, - total_points INTEGER NOT NULL DEFAULT 0, - created_at TIMESTAMP WITH TIME ZONE DEFAULT now(), - updated_at TIMESTAMP WITH TIME ZONE DEFAULT now() -); +-- This file is the main schema definition for the points system -- Create point_transactions table to track individual point awards CREATE TABLE IF NOT EXISTS point_transactions ( id BIGSERIAL PRIMARY KEY, - user_id UUID NOT NULL REFERENCES user_points(user_id), + user_id UUID NOT NULL, points INTEGER NOT NULL, transaction_type TEXT NOT NULL, image_hash TEXT, @@ -28,56 +18,7 @@ CREATE TABLE IF NOT EXISTS point_transactions ( CREATE INDEX IF NOT EXISTS idx_point_transactions_user_id ON point_transactions(user_id); CREATE INDEX IF NOT EXISTS idx_point_transactions_image_hash ON point_transactions(image_hash); --- Create a function to increment user points (matching TypeScript parameter names and order) -CREATE OR REPLACE FUNCTION increment_user_points( - user_id_param TEXT, - points_param INTEGER -) -RETURNS void AS $$ -BEGIN - UPDATE user_points - SET - total_points = total_points + points_param, - updated_at = now() - WHERE user_id = user_id_param::UUID; -END; -$$ LANGUAGE plpgsql; - --- Create a function to get current points for a user (matching TypeScript parameter names and order) -CREATE OR REPLACE FUNCTION get_current_points( - user_id_param TEXT -) -RETURNS INTEGER AS $$ -DECLARE - current_points INTEGER; -BEGIN - SELECT total_points INTO current_points - FROM user_points - WHERE user_id = user_id_param::UUID; - - RETURN COALESCE(current_points, 0); -END; -$$ LANGUAGE plpgsql; - --- Create a function to create a user (matching TypeScript parameter names and order) -CREATE OR REPLACE FUNCTION create_user_points( - user_id_param TEXT, - email_param TEXT, - initial_points INTEGER DEFAULT 0 -) -RETURNS json AS $$ -DECLARE - new_user_record user_points; -BEGIN - INSERT INTO user_points (user_id, email, total_points, created_at, updated_at) - VALUES (user_id_param::UUID, email_param, initial_points, now(), now()) - RETURNING * INTO new_user_record; - - RETURN row_to_json(new_user_record); -END; -$$ LANGUAGE plpgsql SECURITY DEFINER; - --- Create a function to create a point transaction (matching TypeScript parameter names and order) +-- Create a function to create a point transaction CREATE OR REPLACE FUNCTION create_point_transaction( user_id_param TEXT, points_param INTEGER, @@ -117,7 +58,23 @@ BEGIN END; $$ LANGUAGE plpgsql SECURITY DEFINER; --- Create a function to get user transactions (matching TypeScript parameter names and order) +-- Function to calculate a user's total points from transactions +CREATE OR REPLACE FUNCTION get_user_total_points( + user_id_param TEXT +) +RETURNS INTEGER AS $$ +DECLARE + total INTEGER; +BEGIN + SELECT COALESCE(SUM(points), 0) INTO total + FROM point_transactions + WHERE user_id = user_id_param::UUID; + + RETURN total; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +-- Create a function to get user transactions CREATE OR REPLACE FUNCTION get_user_transactions( user_id_param TEXT ) @@ -131,26 +88,31 @@ BEGIN END; $$ LANGUAGE plpgsql SECURITY DEFINER; --- Add Row Level Security (RLS) policies --- Enable RLS -ALTER TABLE user_points ENABLE ROW LEVEL SECURITY; -ALTER TABLE point_transactions ENABLE ROW LEVEL SECURITY; +-- Check if an image has already been processed for a user +CREATE OR REPLACE FUNCTION check_image_exists( + user_id_param TEXT, + image_hash_param TEXT +) +RETURNS BOOLEAN AS $$ +DECLARE + exists_flag BOOLEAN; +BEGIN + SELECT EXISTS ( + SELECT 1 FROM point_transactions + WHERE user_id = user_id_param::UUID + AND image_hash = image_hash_param + ) INTO exists_flag; + + RETURN exists_flag; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; --- Skip policy creation if they already exist (will generate warnings but not fail) --- Comment out policy sections if you've already created them +-- Enable Row Level Security +ALTER TABLE point_transactions ENABLE ROW LEVEL SECURITY; -- Create policies section - uncomment if you need to recreate policies /* --- user_points policies -CREATE POLICY "Users can view their own points" - ON user_points FOR SELECT - USING (auth.uid() = user_id); - -CREATE POLICY "Service role can manage all points" - ON user_points FOR ALL - USING (auth.role() = 'service_role'); - --- point_transactions policies +-- Transaction policies CREATE POLICY "Users can view their own transactions" ON point_transactions FOR SELECT USING (auth.uid() = user_id);