supabase updates

This commit is contained in:
zramsay 2025-03-12 09:04:24 -04:00
parent 47b9f8264f
commit 92333ec559
4 changed files with 266 additions and 948 deletions

View File

@ -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 [];
}
}

View File

@ -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;
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;

View File

@ -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;

View File

@ -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);