mirror of
https://github.com/mito-systems/ranger-app.git
synced 2026-05-08 20:04:07 +00:00
supabase updates
This commit is contained in:
parent
47b9f8264f
commit
92333ec559
@ -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 [];
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user