mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 12:11:11 -05:00
feat: Implement final error coverage
This commit is contained in:
@@ -11,8 +11,7 @@ import type {
|
|||||||
IdentitySafetyCheck,
|
IdentitySafetyCheck,
|
||||||
IdentityOperationResult
|
IdentityOperationResult
|
||||||
} from '@/types/identity';
|
} from '@/types/identity';
|
||||||
import { logger } from './logger';
|
import { handleNonCriticalError, handleError, getErrorMessage } from './errorHandler';
|
||||||
import { getErrorMessage } from './errorHandler';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all identities for the current user
|
* Get all identities for the current user
|
||||||
@@ -25,10 +24,9 @@ export async function getUserIdentities(): Promise<UserIdentity[]> {
|
|||||||
|
|
||||||
return (data?.identities || []) as UserIdentity[];
|
return (data?.identities || []) as UserIdentity[];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = getErrorMessage(error);
|
handleNonCriticalError(error, {
|
||||||
logger.error('Failed to get user identities', {
|
action: 'Get User Identities',
|
||||||
action: 'get_identities',
|
metadata: { returnedEmptyArray: true }
|
||||||
error: errorMsg
|
|
||||||
});
|
});
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -102,9 +100,9 @@ export async function disconnectIdentity(
|
|||||||
// Get AAL level - fail closed on error
|
// Get AAL level - fail closed on error
|
||||||
const { data: aalData, error: aalError } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel();
|
const { data: aalData, error: aalError } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel();
|
||||||
if (aalError) {
|
if (aalError) {
|
||||||
logger.error('Failed to get AAL level for identity disconnect', {
|
handleNonCriticalError(aalError, {
|
||||||
action: 'disconnect_identity_aal_check',
|
action: 'Get AAL Level (Identity Disconnect)',
|
||||||
error: aalError.message
|
metadata: { failClosed: true }
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -120,9 +118,9 @@ export async function disconnectIdentity(
|
|||||||
const { data: factors, error: factorsError } = await supabase.auth.mfa.listFactors();
|
const { data: factors, error: factorsError } = await supabase.auth.mfa.listFactors();
|
||||||
|
|
||||||
if (factorsError) {
|
if (factorsError) {
|
||||||
logger.error('Failed to list MFA factors for identity disconnect', {
|
handleNonCriticalError(factorsError, {
|
||||||
action: 'disconnect_identity_mfa_check',
|
action: 'List MFA Factors (Identity Disconnect)',
|
||||||
error: factorsError.message
|
metadata: { failClosed: true }
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -177,15 +175,13 @@ export async function disconnectIdentity(
|
|||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = getErrorMessage(error);
|
handleError(error, {
|
||||||
logger.error('Failed to disconnect identity', {
|
action: 'Disconnect Identity',
|
||||||
action: 'identity_disconnect',
|
metadata: { provider }
|
||||||
provider,
|
|
||||||
error: errorMsg
|
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: errorMsg
|
error: getErrorMessage(error)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,15 +206,13 @@ export async function connectIdentity(
|
|||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = getErrorMessage(error);
|
handleError(error, {
|
||||||
logger.error('Failed to connect identity', {
|
action: 'Connect Identity',
|
||||||
action: 'identity_connect',
|
metadata: { provider }
|
||||||
provider,
|
|
||||||
error: errorMsg
|
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: errorMsg
|
error: getErrorMessage(error)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,11 +234,6 @@ export async function addPasswordToAccount(): Promise<IdentityOperationResult> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('Initiating password setup', {
|
|
||||||
action: 'password_setup_initiated',
|
|
||||||
email: userEmail
|
|
||||||
});
|
|
||||||
|
|
||||||
// Trigger Supabase password reset email
|
// Trigger Supabase password reset email
|
||||||
// User clicks link and sets password, which automatically creates email identity
|
// User clicks link and sets password, which automatically creates email identity
|
||||||
const { error: resetError } = await supabase.auth.resetPasswordForEmail(
|
const { error: resetError } = await supabase.auth.resetPasswordForEmail(
|
||||||
@@ -255,20 +244,14 @@ export async function addPasswordToAccount(): Promise<IdentityOperationResult> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (resetError) {
|
if (resetError) {
|
||||||
logger.error('Failed to send password reset email', {
|
handleError(resetError, {
|
||||||
|
action: 'Send Password Reset Email',
|
||||||
userId: user?.id,
|
userId: user?.id,
|
||||||
action: 'password_setup_email',
|
metadata: { email: userEmail }
|
||||||
error: resetError.message
|
|
||||||
});
|
});
|
||||||
throw resetError;
|
throw resetError;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('Password reset email sent', {
|
|
||||||
userId: user!.id,
|
|
||||||
action: 'password_setup_initiated',
|
|
||||||
email: userEmail
|
|
||||||
});
|
|
||||||
|
|
||||||
// Log the action
|
// Log the action
|
||||||
await logIdentityChange(user!.id, 'password_setup_initiated', {
|
await logIdentityChange(user!.id, 'password_setup_initiated', {
|
||||||
method: 'reset_password_flow',
|
method: 'reset_password_flow',
|
||||||
@@ -281,14 +264,12 @@ export async function addPasswordToAccount(): Promise<IdentityOperationResult> {
|
|||||||
email: userEmail
|
email: userEmail
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = getErrorMessage(error);
|
handleError(error, {
|
||||||
logger.error('Failed to initiate password setup', {
|
action: 'Initiate Password Setup'
|
||||||
action: 'password_setup',
|
|
||||||
error: errorMsg
|
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: errorMsg
|
error: getErrorMessage(error)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -310,10 +291,10 @@ async function logIdentityChange(
|
|||||||
_details: details
|
_details: details
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to log identity change to audit', {
|
handleNonCriticalError(error, {
|
||||||
|
action: 'Log Identity Change to Audit',
|
||||||
userId,
|
userId,
|
||||||
action,
|
metadata: { auditAction: action }
|
||||||
error: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
});
|
||||||
// Don't fail the operation if audit logging fails
|
// Don't fail the operation if audit logging fails
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { supabase } from '@/lib/supabaseClient';
|
import { supabase } from '@/lib/supabaseClient';
|
||||||
import { invokeWithTracking } from './edgeFunctionTracking';
|
import { invokeWithTracking } from './edgeFunctionTracking';
|
||||||
import type { UploadedImage } from '@/components/upload/EntityMultiImageUploader';
|
import type { UploadedImage } from '@/components/upload/EntityMultiImageUploader';
|
||||||
import { logger } from './logger';
|
import { handleError, handleNonCriticalError } from './errorHandler';
|
||||||
|
|
||||||
export interface CloudflareUploadResponse {
|
export interface CloudflareUploadResponse {
|
||||||
result: {
|
result: {
|
||||||
@@ -34,20 +34,14 @@ export async function uploadPendingImages(images: UploadedImage[]): Promise<Uplo
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (urlError || !uploadUrlData?.uploadURL) {
|
if (urlError || !uploadUrlData?.uploadURL) {
|
||||||
logger.error('Failed to get upload URL', {
|
const error = new Error(`Failed to get upload URL for "${fileName}": ${urlError?.message || 'Unknown error'}`);
|
||||||
action: 'upload_pending_images',
|
handleError(error, {
|
||||||
fileName,
|
action: 'Get Upload URL',
|
||||||
requestId,
|
metadata: { fileName, requestId }
|
||||||
error: urlError?.message || 'Unknown error',
|
|
||||||
});
|
});
|
||||||
throw new Error(`Failed to get upload URL for "${fileName}": ${urlError?.message || 'Unknown error'}`);
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('Got upload URL', {
|
|
||||||
action: 'upload_pending_images',
|
|
||||||
fileName,
|
|
||||||
requestId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Step 2: Upload file directly to Cloudflare
|
// Step 2: Upload file directly to Cloudflare
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
@@ -60,30 +54,25 @@ export async function uploadPendingImages(images: UploadedImage[]): Promise<Uplo
|
|||||||
|
|
||||||
if (!uploadResponse.ok) {
|
if (!uploadResponse.ok) {
|
||||||
const errorText = await uploadResponse.text();
|
const errorText = await uploadResponse.text();
|
||||||
logger.error('Cloudflare upload failed', {
|
const error = new Error(`Upload failed for "${fileName}" (status ${uploadResponse.status}): ${errorText}`);
|
||||||
action: 'upload_pending_images',
|
handleError(error, {
|
||||||
fileName,
|
action: 'Cloudflare Upload',
|
||||||
status: uploadResponse.status,
|
metadata: { fileName, status: uploadResponse.status }
|
||||||
error: errorText,
|
|
||||||
});
|
});
|
||||||
throw new Error(`Upload failed for "${fileName}" (status ${uploadResponse.status}): ${errorText}`);
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result: CloudflareUploadResponse = await uploadResponse.json();
|
const result: CloudflareUploadResponse = await uploadResponse.json();
|
||||||
|
|
||||||
if (!result.success || !result.result) {
|
if (!result.success || !result.result) {
|
||||||
logger.error('Cloudflare upload unsuccessful', {
|
const error = new Error(`Cloudflare upload returned unsuccessful response for "${fileName}"`);
|
||||||
action: 'upload_pending_images',
|
handleError(error, {
|
||||||
fileName,
|
action: 'Cloudflare Upload',
|
||||||
|
metadata: { fileName }
|
||||||
});
|
});
|
||||||
throw new Error(`Cloudflare upload returned unsuccessful response for "${fileName}"`);
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('Image uploaded successfully', {
|
|
||||||
action: 'upload_pending_images',
|
|
||||||
fileName,
|
|
||||||
imageId: result.result.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clean up object URL
|
// Clean up object URL
|
||||||
URL.revokeObjectURL(image.url);
|
URL.revokeObjectURL(image.url);
|
||||||
@@ -132,10 +121,13 @@ export async function uploadPendingImages(images: UploadedImage[]): Promise<Uplo
|
|||||||
// If any uploads failed, clean up ONLY newly uploaded images and throw error
|
// If any uploads failed, clean up ONLY newly uploaded images and throw error
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
if (newlyUploadedImageIds.length > 0) {
|
if (newlyUploadedImageIds.length > 0) {
|
||||||
logger.error('Some uploads failed, cleaning up', {
|
const cleanupError = new Error(`Some uploads failed, cleaning up ${newlyUploadedImageIds.length} newly uploaded images`);
|
||||||
action: 'upload_pending_images',
|
handleError(cleanupError, {
|
||||||
newlyUploadedCount: newlyUploadedImageIds.length,
|
action: 'Upload Cleanup',
|
||||||
failureCount: errors.length,
|
metadata: {
|
||||||
|
newlyUploadedCount: newlyUploadedImageIds.length,
|
||||||
|
failureCount: errors.length
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Attempt cleanup in parallel with detailed error tracking
|
// Attempt cleanup in parallel with detailed error tracking
|
||||||
@@ -148,24 +140,29 @@ export async function uploadPendingImages(images: UploadedImage[]): Promise<Uplo
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Track cleanup failures for better debugging
|
// Track cleanup failures silently (non-critical)
|
||||||
const cleanupFailures = cleanupResults.filter(r => r.status === 'rejected');
|
const cleanupFailures = cleanupResults.filter(r => r.status === 'rejected');
|
||||||
if (cleanupFailures.length > 0) {
|
if (cleanupFailures.length > 0) {
|
||||||
logger.error('Failed to cleanup images', {
|
handleNonCriticalError(
|
||||||
action: 'upload_pending_images_cleanup',
|
new Error(`Failed to cleanup ${cleanupFailures.length} of ${newlyUploadedImageIds.length} images`),
|
||||||
cleanupFailures: cleanupFailures.length,
|
{
|
||||||
totalCleanup: newlyUploadedImageIds.length,
|
action: 'Image Cleanup',
|
||||||
orphanedImages: newlyUploadedImageIds.filter((_, i) => cleanupResults[i].status === 'rejected'),
|
metadata: {
|
||||||
});
|
cleanupFailures: cleanupFailures.length,
|
||||||
} else {
|
totalCleanup: newlyUploadedImageIds.length,
|
||||||
logger.info('Successfully cleaned up images', {
|
orphanedImages: newlyUploadedImageIds.filter((_, i) => cleanupResults[i].status === 'rejected')
|
||||||
action: 'upload_pending_images_cleanup',
|
}
|
||||||
cleanedCount: newlyUploadedImageIds.length,
|
}
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Failed to upload ${errors.length} of ${images.length} images: ${errors.join('; ')}`);
|
const finalError = new Error(`Failed to upload ${errors.length} of ${images.length} images: ${errors.join('; ')}`);
|
||||||
|
handleError(finalError, {
|
||||||
|
action: 'Image Upload',
|
||||||
|
metadata: { failureCount: errors.length, totalCount: images.length }
|
||||||
|
});
|
||||||
|
throw finalError;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the wasNewlyUploaded flag before returning
|
// Remove the wasNewlyUploaded flag before returning
|
||||||
|
|||||||
@@ -9,8 +9,7 @@
|
|||||||
import { SupabaseClient } from '@supabase/supabase-js';
|
import { SupabaseClient } from '@supabase/supabase-js';
|
||||||
import { createTableQuery } from '@/lib/supabaseHelpers';
|
import { createTableQuery } from '@/lib/supabaseHelpers';
|
||||||
import type { ModerationItem } from '@/types/moderation';
|
import type { ModerationItem } from '@/types/moderation';
|
||||||
import { logger } from '@/lib/logger';
|
import { handleError, handleNonCriticalError, getErrorMessage } from '@/lib/errorHandler';
|
||||||
import { getErrorMessage } from '@/lib/errorHandler';
|
|
||||||
import { invokeWithTracking, invokeBatchWithTracking } from '@/lib/edgeFunctionTracking';
|
import { invokeWithTracking, invokeBatchWithTracking } from '@/lib/edgeFunctionTracking';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -154,16 +153,15 @@ export async function approvePhotoSubmission(
|
|||||||
shouldRemoveFromQueue: true,
|
shouldRemoveFromQueue: true,
|
||||||
};
|
};
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const errorMessage = getErrorMessage(error);
|
handleError(error, {
|
||||||
logger.error('Photo approval failed', {
|
action: 'Approve Photo Submission',
|
||||||
action: 'approve_photo',
|
userId: config.moderatorId,
|
||||||
submissionId: config.submissionId,
|
metadata: { submissionId: config.submissionId }
|
||||||
error: errorMessage
|
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Failed to approve photo submission',
|
message: 'Failed to approve photo submission',
|
||||||
error: new Error(errorMessage),
|
error: error instanceof Error ? error : new Error(getErrorMessage(error)),
|
||||||
shouldRemoveFromQueue: false,
|
shouldRemoveFromQueue: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -194,22 +192,14 @@ export async function approveSubmissionItems(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (approvalError) {
|
if (approvalError) {
|
||||||
logger.error('Submission items approval failed via edge function', {
|
const error = new Error(`Failed to process submission items: ${approvalError.message}`);
|
||||||
action: 'approve_submission_items',
|
handleError(error, {
|
||||||
submissionId,
|
action: 'Approve Submission Items',
|
||||||
itemCount: itemIds.length,
|
metadata: { submissionId, itemCount: itemIds.length, requestId }
|
||||||
requestId,
|
|
||||||
error: approvalError.message,
|
|
||||||
});
|
});
|
||||||
throw new Error(`Failed to process submission items: ${approvalError.message}`);
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('Submission items approved successfully', {
|
|
||||||
action: 'approve_submission_items',
|
|
||||||
submissionId,
|
|
||||||
itemCount: itemIds.length,
|
|
||||||
requestId,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -217,17 +207,14 @@ export async function approveSubmissionItems(
|
|||||||
shouldRemoveFromQueue: true,
|
shouldRemoveFromQueue: true,
|
||||||
};
|
};
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const errorMessage = getErrorMessage(error);
|
handleError(error, {
|
||||||
logger.error('Submission items approval failed', {
|
action: 'Approve Submission Items',
|
||||||
action: 'approve_submission_items',
|
metadata: { submissionId, itemCount: itemIds.length }
|
||||||
submissionId,
|
|
||||||
itemCount: itemIds.length,
|
|
||||||
error: errorMessage
|
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Failed to approve submission items',
|
message: 'Failed to approve submission items',
|
||||||
error: new Error(errorMessage),
|
error: error instanceof Error ? error : new Error(getErrorMessage(error)),
|
||||||
shouldRemoveFromQueue: false,
|
shouldRemoveFromQueue: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -260,11 +247,9 @@ export async function rejectSubmissionItems(
|
|||||||
.eq('status', 'pending');
|
.eq('status', 'pending');
|
||||||
|
|
||||||
if (rejectError) {
|
if (rejectError) {
|
||||||
const errorMessage = getErrorMessage(rejectError);
|
handleError(rejectError, {
|
||||||
logger.error('Item rejection cascade failed', {
|
action: 'Reject Submission Items (Cascade)',
|
||||||
action: 'reject_submission_items',
|
metadata: { submissionId }
|
||||||
submissionId,
|
|
||||||
error: errorMessage
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,16 +259,14 @@ export async function rejectSubmissionItems(
|
|||||||
shouldRemoveFromQueue: false, // Parent rejection will handle removal
|
shouldRemoveFromQueue: false, // Parent rejection will handle removal
|
||||||
};
|
};
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const errorMessage = getErrorMessage(error);
|
handleError(error, {
|
||||||
logger.error('Submission items rejection failed', {
|
action: 'Reject Submission Items',
|
||||||
action: 'reject_submission_items',
|
metadata: { submissionId }
|
||||||
submissionId,
|
|
||||||
error: errorMessage
|
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Failed to reject submission items',
|
message: 'Failed to reject submission items',
|
||||||
error: new Error(errorMessage),
|
error: error instanceof Error ? error : new Error(getErrorMessage(error)),
|
||||||
shouldRemoveFromQueue: false,
|
shouldRemoveFromQueue: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -412,17 +395,15 @@ export async function performModerationAction(
|
|||||||
shouldRemoveFromQueue: action === 'approved' || action === 'rejected',
|
shouldRemoveFromQueue: action === 'approved' || action === 'rejected',
|
||||||
};
|
};
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const errorMessage = getErrorMessage(error);
|
handleError(error, {
|
||||||
logger.error('Moderation action failed', {
|
action: `${config.action === 'approved' ? 'Approve' : 'Reject'} Content`,
|
||||||
action: config.action,
|
userId: config.moderatorId,
|
||||||
itemType: item.type,
|
metadata: { itemType: item.type, itemId: item.id }
|
||||||
itemId: item.id,
|
|
||||||
error: errorMessage
|
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: `Failed to ${config.action} content`,
|
message: `Failed to ${config.action} content`,
|
||||||
error: new Error(errorMessage),
|
error: error instanceof Error ? error : new Error(getErrorMessage(error)),
|
||||||
shouldRemoveFromQueue: false,
|
shouldRemoveFromQueue: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -513,15 +494,20 @@ export async function deleteSubmission(
|
|||||||
const successfulDeletions = deleteResults.filter(r => !r.error);
|
const successfulDeletions = deleteResults.filter(r => !r.error);
|
||||||
deletedPhotoCount = successfulDeletions.length;
|
deletedPhotoCount = successfulDeletions.length;
|
||||||
|
|
||||||
// Log any failures
|
// Log any failures silently (background operation)
|
||||||
const failedDeletions = deleteResults.filter(r => r.error);
|
const failedDeletions = deleteResults.filter(r => r.error);
|
||||||
if (failedDeletions.length > 0) {
|
if (failedDeletions.length > 0) {
|
||||||
logger.error('Some photo deletions failed', {
|
handleNonCriticalError(
|
||||||
action: 'delete_submission_photos',
|
new Error(`Failed to delete ${failedDeletions.length} of ${validImageIds.length} photos`),
|
||||||
failureCount: failedDeletions.length,
|
{
|
||||||
totalAttempted: validImageIds.length,
|
action: 'Delete Submission Photos',
|
||||||
failedRequestIds: failedDeletions.map(r => r.requestId),
|
metadata: {
|
||||||
});
|
failureCount: failedDeletions.length,
|
||||||
|
totalAttempted: validImageIds.length,
|
||||||
|
failedRequestIds: failedDeletions.map(r => r.requestId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -563,12 +549,15 @@ export async function deleteSubmission(
|
|||||||
message,
|
message,
|
||||||
shouldRemoveFromQueue: true,
|
shouldRemoveFromQueue: true,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
logger.error('Error deleting submission', { error, submissionId: item.id });
|
handleError(error, {
|
||||||
|
action: 'Delete Submission',
|
||||||
|
metadata: { submissionId: item.id, deletePhotos }
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Failed to delete submission',
|
message: 'Failed to delete submission',
|
||||||
error: error as Error,
|
error: error instanceof Error ? error : new Error('Unknown error'),
|
||||||
shouldRemoveFromQueue: false,
|
shouldRemoveFromQueue: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import type { ModerationAction } from '../moderationStateMachine';
|
|||||||
import { hasActiveLock, needsLockRenewal } from '../moderationStateMachine';
|
import { hasActiveLock, needsLockRenewal } from '../moderationStateMachine';
|
||||||
import { toast } from '@/hooks/use-toast';
|
import { toast } from '@/hooks/use-toast';
|
||||||
import { supabase } from '@/lib/supabaseClient';
|
import { supabase } from '@/lib/supabaseClient';
|
||||||
import { logger } from '../logger';
|
import { handleNonCriticalError } from '../errorHandler';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook to monitor lock status and warn about expiry
|
* Hook to monitor lock status and warn about expiry
|
||||||
@@ -33,14 +33,6 @@ export function useLockMonitor(
|
|||||||
|
|
||||||
const checkInterval = setInterval(() => {
|
const checkInterval = setInterval(() => {
|
||||||
if (needsLockRenewal(state)) {
|
if (needsLockRenewal(state)) {
|
||||||
logger.warn('Lock expiring soon', {
|
|
||||||
action: 'lock_expiry_warning',
|
|
||||||
itemId,
|
|
||||||
lockExpires: state.status === 'locked' || state.status === 'reviewing'
|
|
||||||
? state.lockExpires
|
|
||||||
: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Dispatch lock expiry warning
|
// Dispatch lock expiry warning
|
||||||
dispatch({ type: 'LOCK_EXPIRED' });
|
dispatch({ type: 'LOCK_EXPIRED' });
|
||||||
|
|
||||||
@@ -103,16 +95,10 @@ export async function handleExtendLock(
|
|||||||
title: 'Lock Extended',
|
title: 'Lock Extended',
|
||||||
description: 'You have 15 more minutes to complete your review.',
|
description: 'You have 15 more minutes to complete your review.',
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info('Lock extended successfully', {
|
|
||||||
action: 'lock_extended',
|
|
||||||
submissionId,
|
|
||||||
});
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logger.error('Failed to extend lock', {
|
handleNonCriticalError(error, {
|
||||||
action: 'extend_lock_error',
|
action: 'Extend Lock',
|
||||||
submissionId,
|
metadata: { submissionId }
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { supabase } from "@/lib/supabaseClient";
|
import { supabase } from "@/lib/supabaseClient";
|
||||||
import { invokeWithTracking } from "@/lib/edgeFunctionTracking";
|
import { invokeWithTracking } from "@/lib/edgeFunctionTracking";
|
||||||
import { logger } from "@/lib/logger";
|
import { handleNonCriticalError, AppError } from "@/lib/errorHandler";
|
||||||
import { AppError } from "@/lib/errorHandler";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import type {
|
import type {
|
||||||
NotificationPayload,
|
NotificationPayload,
|
||||||
@@ -29,9 +28,9 @@ class NotificationService {
|
|||||||
|
|
||||||
return !!data?.setting_value;
|
return !!data?.setting_value;
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logger.error('Failed to check Novu status', {
|
handleNonCriticalError(error, {
|
||||||
action: 'check_novu_status',
|
action: 'Check Novu Status',
|
||||||
error: error instanceof Error ? error.message : String(error)
|
metadata: { returnedFalse: true }
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -47,10 +46,6 @@ class NotificationService {
|
|||||||
|
|
||||||
const novuEnabled = await this.isNovuEnabled();
|
const novuEnabled = await this.isNovuEnabled();
|
||||||
if (!novuEnabled) {
|
if (!novuEnabled) {
|
||||||
logger.warn('Novu not configured, skipping subscriber update', {
|
|
||||||
action: 'update_novu_subscriber',
|
|
||||||
userId: validated.subscriberId
|
|
||||||
});
|
|
||||||
return { success: false, error: 'Novu not configured' };
|
return { success: false, error: 'Novu not configured' };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,11 +55,10 @@ class NotificationService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
logger.error('Edge function error updating Novu subscriber', {
|
handleNonCriticalError(error, {
|
||||||
action: 'update_novu_subscriber',
|
action: 'Update Novu Subscriber (Edge Function)',
|
||||||
userId: validated.subscriberId,
|
userId: validated.subscriberId,
|
||||||
requestId,
|
metadata: { requestId }
|
||||||
error: error.message
|
|
||||||
});
|
});
|
||||||
throw new AppError(
|
throw new AppError(
|
||||||
'Failed to update notification subscriber',
|
'Failed to update notification subscriber',
|
||||||
@@ -73,18 +67,11 @@ class NotificationService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('Novu subscriber updated successfully', {
|
|
||||||
action: 'update_novu_subscriber',
|
|
||||||
userId: validated.subscriberId,
|
|
||||||
requestId
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logger.error('Error in updateSubscriber', {
|
handleNonCriticalError(error, {
|
||||||
action: 'update_novu_subscriber',
|
action: 'Update Novu Subscriber',
|
||||||
userId: subscriberData.subscriberId,
|
userId: subscriberData.subscriberId
|
||||||
error: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -104,10 +91,6 @@ class NotificationService {
|
|||||||
|
|
||||||
const novuEnabled = await this.isNovuEnabled();
|
const novuEnabled = await this.isNovuEnabled();
|
||||||
if (!novuEnabled) {
|
if (!novuEnabled) {
|
||||||
logger.warn('Novu not configured, skipping subscriber creation', {
|
|
||||||
action: 'create_novu_subscriber',
|
|
||||||
userId: validated.subscriberId
|
|
||||||
});
|
|
||||||
return { success: false, error: 'Novu not configured' };
|
return { success: false, error: 'Novu not configured' };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,11 +100,10 @@ class NotificationService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
logger.error('Edge function error creating Novu subscriber', {
|
handleNonCriticalError(error, {
|
||||||
action: 'create_novu_subscriber',
|
action: 'Create Novu Subscriber (Edge Function)',
|
||||||
userId: validated.subscriberId,
|
userId: validated.subscriberId,
|
||||||
requestId,
|
metadata: { requestId }
|
||||||
error: error.message
|
|
||||||
});
|
});
|
||||||
throw new AppError(
|
throw new AppError(
|
||||||
'Failed to create notification subscriber',
|
'Failed to create notification subscriber',
|
||||||
@@ -146,27 +128,18 @@ class NotificationService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (dbError) {
|
if (dbError) {
|
||||||
logger.error('Failed to store subscriber preferences', {
|
handleNonCriticalError(dbError, {
|
||||||
action: 'store_subscriber_preferences',
|
action: 'Store Subscriber Preferences',
|
||||||
userId: validated.subscriberId,
|
userId: validated.subscriberId
|
||||||
error: dbError.message,
|
|
||||||
errorCode: dbError.code
|
|
||||||
});
|
});
|
||||||
throw dbError;
|
throw dbError;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('Novu subscriber created successfully', {
|
|
||||||
action: 'create_novu_subscriber',
|
|
||||||
userId: validated.subscriberId,
|
|
||||||
requestId
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logger.error('Error in createSubscriber', {
|
handleNonCriticalError(error, {
|
||||||
action: 'create_novu_subscriber',
|
action: 'Create Novu Subscriber',
|
||||||
userId: subscriberData.subscriberId,
|
userId: subscriberData.subscriberId
|
||||||
error: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -207,11 +180,10 @@ class NotificationService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (novuError) {
|
if (novuError) {
|
||||||
logger.error('Failed to update Novu preferences', {
|
handleNonCriticalError(novuError, {
|
||||||
action: 'update_novu_preferences',
|
action: 'Update Novu Preferences',
|
||||||
userId,
|
userId,
|
||||||
requestId,
|
metadata: { requestId }
|
||||||
error: novuError.message
|
|
||||||
});
|
});
|
||||||
throw novuError;
|
throw novuError;
|
||||||
}
|
}
|
||||||
@@ -228,11 +200,9 @@ class NotificationService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (dbError) {
|
if (dbError) {
|
||||||
logger.error('Failed to save notification preferences', {
|
handleNonCriticalError(dbError, {
|
||||||
action: 'save_notification_preferences',
|
action: 'Save Notification Preferences',
|
||||||
userId,
|
userId
|
||||||
error: dbError.message,
|
|
||||||
errorCode: dbError.code
|
|
||||||
});
|
});
|
||||||
throw dbError;
|
throw dbError;
|
||||||
}
|
}
|
||||||
@@ -268,17 +238,11 @@ class NotificationService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('Notification preferences updated', {
|
|
||||||
action: 'update_notification_preferences',
|
|
||||||
userId
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logger.error('Error updating notification preferences', {
|
handleNonCriticalError(error, {
|
||||||
action: 'update_notification_preferences',
|
action: 'Update Notification Preferences',
|
||||||
userId,
|
userId
|
||||||
error: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error instanceof z.ZodError) {
|
if (error instanceof z.ZodError) {
|
||||||
@@ -307,20 +271,14 @@ class NotificationService {
|
|||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
|
|
||||||
if (error && error.code !== 'PGRST116') {
|
if (error && error.code !== 'PGRST116') {
|
||||||
logger.error('Failed to fetch notification preferences', {
|
handleNonCriticalError(error, {
|
||||||
action: 'fetch_notification_preferences',
|
action: 'Fetch Notification Preferences',
|
||||||
userId,
|
userId
|
||||||
error: error.message,
|
|
||||||
errorCode: error.code
|
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
logger.info('No preferences found, returning defaults', {
|
|
||||||
action: 'fetch_notification_preferences',
|
|
||||||
userId
|
|
||||||
});
|
|
||||||
return DEFAULT_NOTIFICATION_PREFERENCES;
|
return DEFAULT_NOTIFICATION_PREFERENCES;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,10 +289,9 @@ class NotificationService {
|
|||||||
frequencySettings: data.frequency_settings
|
frequencySettings: data.frequency_settings
|
||||||
});
|
});
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logger.error('Error fetching notification preferences', {
|
handleNonCriticalError(error, {
|
||||||
action: 'fetch_notification_preferences',
|
action: 'Get Notification Preferences',
|
||||||
userId,
|
userId
|
||||||
error: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -352,10 +309,8 @@ class NotificationService {
|
|||||||
.order('category', { ascending: true });
|
.order('category', { ascending: true });
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
logger.error('Failed to fetch notification templates', {
|
handleNonCriticalError(error, {
|
||||||
action: 'fetch_notification_templates',
|
action: 'Fetch Notification Templates'
|
||||||
error: error.message,
|
|
||||||
errorCode: error.code
|
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -367,9 +322,8 @@ class NotificationService {
|
|||||||
novu_workflow_id: t.novu_workflow_id || null,
|
novu_workflow_id: t.novu_workflow_id || null,
|
||||||
}));
|
}));
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logger.error('Error fetching notification templates', {
|
handleNonCriticalError(error, {
|
||||||
action: 'fetch_notification_templates',
|
action: 'Get Notification Templates'
|
||||||
error: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
});
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -382,11 +336,6 @@ class NotificationService {
|
|||||||
try {
|
try {
|
||||||
const novuEnabled = await this.isNovuEnabled();
|
const novuEnabled = await this.isNovuEnabled();
|
||||||
if (!novuEnabled) {
|
if (!novuEnabled) {
|
||||||
logger.warn('Novu not configured, skipping notification', {
|
|
||||||
action: 'trigger_notification',
|
|
||||||
workflowId: payload.workflowId,
|
|
||||||
subscriberId: payload.subscriberId
|
|
||||||
});
|
|
||||||
return { success: false, error: 'Novu not configured' };
|
return { success: false, error: 'Novu not configured' };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,31 +345,18 @@ class NotificationService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
logger.error('Failed to trigger notification', {
|
handleNonCriticalError(error, {
|
||||||
action: 'trigger_notification',
|
action: 'Trigger Notification',
|
||||||
workflowId: payload.workflowId,
|
metadata: { workflowId: payload.workflowId, subscriberId: payload.subscriberId, requestId }
|
||||||
subscriberId: payload.subscriberId,
|
|
||||||
requestId,
|
|
||||||
error: error.message
|
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('Notification triggered successfully', {
|
|
||||||
action: 'trigger_notification',
|
|
||||||
workflowId: payload.workflowId,
|
|
||||||
subscriberId: payload.subscriberId,
|
|
||||||
transactionId: data?.transactionId,
|
|
||||||
requestId
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logger.error('Error triggering notification', {
|
handleNonCriticalError(error, {
|
||||||
action: 'trigger_notification',
|
action: 'Trigger Notification',
|
||||||
workflowId: payload.workflowId,
|
metadata: { workflowId: payload.workflowId, subscriberId: payload.subscriberId }
|
||||||
subscriberId: payload.subscriberId,
|
|
||||||
error: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -446,25 +382,16 @@ class NotificationService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
logger.error('Failed to notify moderators', {
|
handleNonCriticalError(error, {
|
||||||
action: 'notify_moderators',
|
action: 'Notify Moderators (Submission)',
|
||||||
submissionId: payload.submission_id,
|
metadata: { submissionId: payload.submission_id, requestId }
|
||||||
requestId,
|
|
||||||
error: error.message
|
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('Moderators notified successfully', {
|
|
||||||
action: 'notify_moderators',
|
|
||||||
submissionId: payload.submission_id,
|
|
||||||
requestId
|
|
||||||
});
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logger.error('Error notifying moderators', {
|
handleNonCriticalError(error, {
|
||||||
action: 'notify_moderators',
|
action: 'Notify Moderators (Submission)',
|
||||||
submissionId: payload.submission_id,
|
metadata: { submissionId: payload.submission_id }
|
||||||
error: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -482,10 +409,6 @@ class NotificationService {
|
|||||||
try {
|
try {
|
||||||
const novuEnabled = await this.isNovuEnabled();
|
const novuEnabled = await this.isNovuEnabled();
|
||||||
if (!novuEnabled) {
|
if (!novuEnabled) {
|
||||||
logger.warn('Novu not configured, skipping system announcement', {
|
|
||||||
action: 'send_system_announcement',
|
|
||||||
title: payload.title
|
|
||||||
});
|
|
||||||
return { success: false, error: 'Novu not configured' };
|
return { success: false, error: 'Novu not configured' };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -495,31 +418,21 @@ class NotificationService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
logger.error('Failed to send system announcement', {
|
handleNonCriticalError(error, {
|
||||||
action: 'send_system_announcement',
|
action: 'Send System Announcement',
|
||||||
title: payload.title,
|
metadata: { title: payload.title, requestId }
|
||||||
requestId,
|
|
||||||
error: error.message
|
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('System announcement sent successfully', {
|
|
||||||
action: 'send_system_announcement',
|
|
||||||
title: payload.title,
|
|
||||||
announcementId: data?.announcementId,
|
|
||||||
requestId
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
announcementId: data?.announcementId
|
announcementId: data?.announcementId
|
||||||
};
|
};
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logger.error('Error sending system announcement', {
|
handleNonCriticalError(error, {
|
||||||
action: 'send_system_announcement',
|
action: 'Send System Announcement',
|
||||||
title: payload.title,
|
metadata: { title: payload.title }
|
||||||
error: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -545,10 +458,6 @@ class NotificationService {
|
|||||||
try {
|
try {
|
||||||
const novuEnabled = await this.isNovuEnabled();
|
const novuEnabled = await this.isNovuEnabled();
|
||||||
if (!novuEnabled) {
|
if (!novuEnabled) {
|
||||||
logger.warn('Novu not configured, skipping report notification', {
|
|
||||||
action: 'notify_moderators_report',
|
|
||||||
reportId: payload.reportId
|
|
||||||
});
|
|
||||||
return { success: false, error: 'Novu not configured' };
|
return { success: false, error: 'Novu not configured' };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -558,27 +467,18 @@ class NotificationService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
logger.error('Failed to notify moderators about report', {
|
handleNonCriticalError(error, {
|
||||||
action: 'notify_moderators_report',
|
action: 'Notify Moderators (Report)',
|
||||||
reportId: payload.reportId,
|
metadata: { reportId: payload.reportId, requestId }
|
||||||
requestId,
|
|
||||||
error: error.message
|
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('Moderators notified about report successfully', {
|
|
||||||
action: 'notify_moderators_report',
|
|
||||||
reportId: payload.reportId,
|
|
||||||
requestId
|
|
||||||
});
|
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logger.error('Error notifying moderators about report', {
|
handleNonCriticalError(error, {
|
||||||
action: 'notify_moderators_report',
|
action: 'Notify Moderators (Report)',
|
||||||
reportId: payload.reportId,
|
metadata: { reportId: payload.reportId }
|
||||||
error: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ async function logRequestMetadata(metadata: RequestMetadata): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
logger.error('Failed to log metadata to database', { error, context: 'RequestTracking' });
|
// Already logged by handleNonCriticalError in requestTracking
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { supabase } from '@/lib/supabaseClient';
|
import { supabase } from '@/lib/supabaseClient';
|
||||||
import { logger } from './logger';
|
import { handleError } from './errorHandler';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a URL-safe slug from a name
|
* Generate a URL-safe slug from a name
|
||||||
@@ -51,7 +51,10 @@ export async function ensureUniqueSlug(
|
|||||||
const { data, error } = await query.limit(1);
|
const { data, error } = await query.limit(1);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
logger.error('Error checking slug uniqueness', { error, tableName });
|
handleError(error, {
|
||||||
|
action: 'Check Slug Uniqueness',
|
||||||
|
metadata: { tableName, slug }
|
||||||
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ import type {
|
|||||||
RideModelSubmissionData
|
RideModelSubmissionData
|
||||||
} from '@/types/submission-data';
|
} from '@/types/submission-data';
|
||||||
import { supabase } from '@/lib/supabaseClient';
|
import { supabase } from '@/lib/supabaseClient';
|
||||||
import { logger } from './logger';
|
import { handleNonCriticalError, getErrorMessage } from './errorHandler';
|
||||||
import { getErrorMessage } from './errorHandler';
|
|
||||||
|
|
||||||
type SubmissionDataTypes =
|
type SubmissionDataTypes =
|
||||||
| ParkSubmissionData
|
| ParkSubmissionData
|
||||||
@@ -81,11 +80,9 @@ async function detectPhotoChanges(submissionId: string): Promise<PhotoChange[]>
|
|||||||
.eq('submission_id', submissionId);
|
.eq('submission_id', submissionId);
|
||||||
|
|
||||||
if (photoError) {
|
if (photoError) {
|
||||||
const errorMessage = getErrorMessage(photoError);
|
handleNonCriticalError(photoError, {
|
||||||
logger.error('Photo submission fetch failed', {
|
action: 'Detect Photo Changes (Fetch Photo Submission)',
|
||||||
action: 'detect_photo_changes',
|
metadata: { submissionId }
|
||||||
submissionId,
|
|
||||||
error: errorMessage
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const photoSubmission = photoSubmissions?.[0];
|
const photoSubmission = photoSubmissions?.[0];
|
||||||
@@ -109,11 +106,9 @@ async function detectPhotoChanges(submissionId: string): Promise<PhotoChange[]>
|
|||||||
.in('item_type', ['photo_edit', 'photo_delete']);
|
.in('item_type', ['photo_edit', 'photo_delete']);
|
||||||
|
|
||||||
if (itemsError) {
|
if (itemsError) {
|
||||||
const errorMessage = getErrorMessage(itemsError);
|
handleNonCriticalError(itemsError, {
|
||||||
logger.error('Submission items fetch failed', {
|
action: 'Detect Photo Changes (Fetch Submission Items)',
|
||||||
action: 'detect_photo_changes',
|
metadata: { submissionId }
|
||||||
submissionId,
|
|
||||||
error: errorMessage
|
|
||||||
});
|
});
|
||||||
} else if (submissionItems && submissionItems.length > 0) {
|
} else if (submissionItems && submissionItems.length > 0) {
|
||||||
for (const item of submissionItems) {
|
for (const item of submissionItems) {
|
||||||
@@ -123,11 +118,9 @@ async function detectPhotoChanges(submissionId: string): Promise<PhotoChange[]>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const errorMessage = getErrorMessage(err);
|
handleNonCriticalError(err, {
|
||||||
logger.error('Photo change detection failed', {
|
action: 'Detect Photo Changes',
|
||||||
action: 'detect_photo_changes',
|
metadata: { submissionId }
|
||||||
submissionId,
|
|
||||||
error: errorMessage
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,7 +342,10 @@ export async function detectChanges(
|
|||||||
if (data?.name) entityName = `${data.name} (${formatEntityType(entityType)})`;
|
if (data?.name) entityName = `${data.name} (${formatEntityType(entityType)})`;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Error fetching entity name for photo operation', { error: err, entityType: itemData.entity_type, entityId: itemData.entity_id });
|
handleNonCriticalError(err, {
|
||||||
|
action: 'Fetch Entity Name for Photo Operation',
|
||||||
|
metadata: { entityType: itemData.entity_type, entityId: itemData.entity_id }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,7 +391,10 @@ export async function detectChanges(
|
|||||||
entityName = `${formatEntityType(entityType)} - ${itemData.title}`;
|
entityName = `${formatEntityType(entityType)} - ${itemData.title}`;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Error fetching entity name for milestone', { error: err, entityType: itemData.entity_type, entityId: itemData.entity_id });
|
handleNonCriticalError(err, {
|
||||||
|
action: 'Fetch Entity Name for Milestone',
|
||||||
|
metadata: { entityType: itemData.entity_type, entityId: itemData.entity_id }
|
||||||
|
});
|
||||||
// Fall back to just the title if database lookup fails
|
// Fall back to just the title if database lookup fails
|
||||||
if (itemData.title) {
|
if (itemData.title) {
|
||||||
entityName = itemData.title;
|
entityName = itemData.title;
|
||||||
@@ -434,7 +433,10 @@ export async function detectChanges(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Error resolving entity name for field display', { error: err, entityType: itemData.entity_type, entityId: itemData.entity_id });
|
handleNonCriticalError(err, {
|
||||||
|
action: 'Resolve Entity Name for Field Display',
|
||||||
|
metadata: { entityType: itemData.entity_type, entityId: itemData.entity_id }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add entity name as an explicit field change at the beginning
|
// Add entity name as an explicit field change at the beginning
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { supabase } from '@/lib/supabaseClient';
|
import { supabase } from '@/lib/supabaseClient';
|
||||||
import { getErrorMessage } from './errorHandler';
|
import { handleError, handleNonCriticalError, getErrorMessage } from './errorHandler';
|
||||||
import { logger } from './logger';
|
|
||||||
import { extractCloudflareImageId } from './cloudflareImageUtils';
|
import { extractCloudflareImageId } from './cloudflareImageUtils';
|
||||||
|
|
||||||
// Core submission item interface with dependencies
|
// Core submission item interface with dependencies
|
||||||
@@ -297,21 +296,19 @@ export async function approveSubmissionItems(
|
|||||||
dependencyMap.set(item.id, entityId);
|
dependencyMap.set(item.id, entityId);
|
||||||
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const errorMsg = getErrorMessage(error);
|
handleError(error, {
|
||||||
logger.error('Error approving items', {
|
action: 'Approve Submission Items',
|
||||||
action: 'approve_submission_items',
|
|
||||||
error: errorMsg,
|
|
||||||
userId,
|
userId,
|
||||||
itemCount: items.length
|
metadata: { itemCount: items.length, itemType: item.item_type }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update item with error status
|
// Update item with error status
|
||||||
await updateSubmissionItem(item.id, {
|
await updateSubmissionItem(item.id, {
|
||||||
status: 'rejected' as const,
|
status: 'rejected' as const,
|
||||||
rejection_reason: `Failed to create entity: ${errorMsg}`,
|
rejection_reason: `Failed to create entity: ${getErrorMessage(error)}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
throw new Error(`Failed to approve ${item.item_type}: ${errorMsg}`);
|
throw new Error(`Failed to approve ${item.item_type}: ${getErrorMessage(error)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -338,7 +335,6 @@ async function createVersionForApprovedItem(
|
|||||||
// - app.current_user_id = original submitter
|
// - app.current_user_id = original submitter
|
||||||
// - app.submission_id = submission ID
|
// - app.submission_id = submission ID
|
||||||
// Then the trigger creates the version automatically
|
// Then the trigger creates the version automatically
|
||||||
logger.debug('Version will be created automatically by trigger', { itemType, entityId });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -452,10 +448,9 @@ async function createPark(data: any, dependencyMap: Map<string, string>, sortedI
|
|||||||
.eq('id', data.park_id);
|
.eq('id', data.park_id);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
logger.error('Error updating park', {
|
handleError(error, {
|
||||||
action: 'update_park',
|
action: 'Update Park',
|
||||||
parkId: data.park_id,
|
metadata: { parkId: data.park_id, parkName: resolvedData.name }
|
||||||
error: error.message
|
|
||||||
});
|
});
|
||||||
throw new Error(`Database error: ${error.message}`);
|
throw new Error(`Database error: ${error.message}`);
|
||||||
}
|
}
|
||||||
@@ -495,10 +490,9 @@ async function createPark(data: any, dependencyMap: Map<string, string>, sortedI
|
|||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
logger.error('Error creating park', {
|
handleError(error, {
|
||||||
action: 'create_park',
|
action: 'Create Park',
|
||||||
parkName: resolvedData.name,
|
metadata: { parkName: resolvedData.name }
|
||||||
error: error.message
|
|
||||||
});
|
});
|
||||||
throw new Error(`Database error: ${error.message}`);
|
throw new Error(`Database error: ${error.message}`);
|
||||||
}
|
}
|
||||||
@@ -551,10 +545,9 @@ async function resolveLocationId(locationData: any): Promise<string | null> {
|
|||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
logger.error('Error creating location', {
|
handleError(error, {
|
||||||
action: 'create_location',
|
action: 'Create Location',
|
||||||
locationData,
|
metadata: { locationData }
|
||||||
error: error.message
|
|
||||||
});
|
});
|
||||||
throw new Error(`Failed to create location: ${error.message}`);
|
throw new Error(`Failed to create location: ${error.message}`);
|
||||||
}
|
}
|
||||||
@@ -611,7 +604,10 @@ async function createRide(data: any, dependencyMap: Map<string, string>, sortedI
|
|||||||
.eq('id', data.ride_id);
|
.eq('id', data.ride_id);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
logger.error('Error updating ride', { error: error.message, rideId: data.ride_id });
|
handleError(error, {
|
||||||
|
action: 'Update Ride',
|
||||||
|
metadata: { rideId: data.ride_id, rideName: resolvedData.name }
|
||||||
|
});
|
||||||
throw new Error(`Database error: ${error.message}`);
|
throw new Error(`Database error: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -642,7 +638,10 @@ async function createRide(data: any, dependencyMap: Map<string, string>, sortedI
|
|||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
logger.error('Error creating ride', { error: error.message, rideName: resolvedData.name });
|
handleError(error, {
|
||||||
|
action: 'Create Ride',
|
||||||
|
metadata: { rideName: resolvedData.name }
|
||||||
|
});
|
||||||
throw new Error(`Database error: ${error.message}`);
|
throw new Error(`Database error: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -686,7 +685,10 @@ async function createCompany(
|
|||||||
.eq('id', data.id);
|
.eq('id', data.id);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
logger.error('Error updating company', { error: error.message, companyId: data.id });
|
handleError(error, {
|
||||||
|
action: 'Update Company',
|
||||||
|
metadata: { companyId: data.id, companyName: resolvedData.name, companyType }
|
||||||
|
});
|
||||||
throw new Error(`Database error: ${error.message}`);
|
throw new Error(`Database error: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -721,7 +723,10 @@ async function createCompany(
|
|||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
logger.error('Error creating company', { error: error.message, companyName: resolvedData.name, companyType });
|
handleError(error, {
|
||||||
|
action: 'Create Company',
|
||||||
|
metadata: { companyName: resolvedData.name, companyType }
|
||||||
|
});
|
||||||
throw new Error(`Database error: ${error.message}`);
|
throw new Error(`Database error: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -760,7 +765,10 @@ async function createRideModel(data: any, dependencyMap: Map<string, string>, so
|
|||||||
.eq('id', data.ride_model_id);
|
.eq('id', data.ride_model_id);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
logger.error('Error updating ride model', { error: error.message, rideModelId: data.ride_model_id });
|
handleError(error, {
|
||||||
|
action: 'Update Ride Model',
|
||||||
|
metadata: { rideModelId: data.ride_model_id, modelName: resolvedData.name }
|
||||||
|
});
|
||||||
throw new Error(`Database error: ${error.message}`);
|
throw new Error(`Database error: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -797,7 +805,10 @@ async function createRideModel(data: any, dependencyMap: Map<string, string>, so
|
|||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
logger.error('Error creating ride model', { error: error.message, modelName: resolvedData.name });
|
handleError(error, {
|
||||||
|
action: 'Create Ride Model',
|
||||||
|
metadata: { modelName: resolvedData.name }
|
||||||
|
});
|
||||||
throw new Error(`Database error: ${error.message}`);
|
throw new Error(`Database error: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -876,7 +887,10 @@ async function approvePhotos(data: any, dependencyMap: Map<string, string>, user
|
|||||||
.select();
|
.select();
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
logger.error('Error inserting photos', { error: error.message, photoCount: photosToInsert.length, entityType, entityId: finalEntityId });
|
handleError(error, {
|
||||||
|
action: 'Insert Photos',
|
||||||
|
metadata: { photoCount: photosToInsert.length, entityType, entityId: finalEntityId }
|
||||||
|
});
|
||||||
throw new Error(`Database error: ${error.message}`);
|
throw new Error(`Database error: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -958,7 +972,10 @@ async function updateEntityFeaturedImage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error updating entity featured image', { error, entityType, entityId });
|
handleNonCriticalError(error, {
|
||||||
|
action: 'Update Entity Featured Image',
|
||||||
|
metadata: { entityType, entityId }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1136,10 +1153,13 @@ export async function rejectSubmissionItems(
|
|||||||
})
|
})
|
||||||
.eq('id', itemId);
|
.eq('id', itemId);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
logger.error('Error rejecting item', { error, itemId });
|
handleNonCriticalError(error, {
|
||||||
throw error;
|
action: 'Reject Submission Item',
|
||||||
}
|
metadata: { itemId }
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all(updates);
|
await Promise.all(updates);
|
||||||
@@ -1171,7 +1191,10 @@ async function updateSubmissionStatusAfterRejection(submissionId: string): Promi
|
|||||||
.eq('submission_id', submissionId);
|
.eq('submission_id', submissionId);
|
||||||
|
|
||||||
if (fetchError) {
|
if (fetchError) {
|
||||||
logger.error('Error fetching submission items', { error: fetchError, submissionId });
|
handleNonCriticalError(fetchError, {
|
||||||
|
action: 'Fetch Submission Items for Status Update',
|
||||||
|
metadata: { submissionId }
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1202,7 +1225,10 @@ async function updateSubmissionStatusAfterRejection(submissionId: string): Promi
|
|||||||
.eq('id', submissionId);
|
.eq('id', submissionId);
|
||||||
|
|
||||||
if (updateError) {
|
if (updateError) {
|
||||||
logger.error('Error updating submission status', { error: updateError, submissionId });
|
handleNonCriticalError(updateError, {
|
||||||
|
action: 'Update Submission Status After Rejection',
|
||||||
|
metadata: { submissionId, newStatus }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1273,10 +1299,9 @@ export async function editSubmissionItem(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (historyError) {
|
if (historyError) {
|
||||||
logger.error('Failed to record edit history', {
|
handleNonCriticalError(historyError, {
|
||||||
itemId,
|
action: 'Record Edit History',
|
||||||
editorId: userId,
|
metadata: { itemId, editorId: userId }
|
||||||
error: historyError.message,
|
|
||||||
});
|
});
|
||||||
// Don't fail the whole operation if history tracking fails
|
// Don't fail the whole operation if history tracking fails
|
||||||
}
|
}
|
||||||
@@ -1293,10 +1318,12 @@ export async function editSubmissionItem(
|
|||||||
true // isEdit = true
|
true // isEdit = true
|
||||||
);
|
);
|
||||||
} catch (versionError) {
|
} catch (versionError) {
|
||||||
logger.error('Failed to create version for manual edit', {
|
handleNonCriticalError(versionError, {
|
||||||
action: 'create_version_for_edit',
|
action: 'Create Version for Manual Edit',
|
||||||
itemType: currentItem.item_type,
|
metadata: {
|
||||||
entityId: currentItem.approved_entity_id
|
itemType: currentItem.item_type,
|
||||||
|
entityId: currentItem.approved_entity_id
|
||||||
|
}
|
||||||
});
|
});
|
||||||
// Don't fail the entire operation, just log the error
|
// Don't fail the entire operation, just log the error
|
||||||
// The edit itself is still saved, just without version history
|
// The edit itself is still saved, just without version history
|
||||||
@@ -1390,7 +1417,10 @@ export async function escalateSubmission(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (auditError) {
|
} catch (auditError) {
|
||||||
logger.error('Failed to log escalation audit', { error: auditError });
|
handleNonCriticalError(auditError, {
|
||||||
|
action: 'Log Escalation Audit',
|
||||||
|
metadata: { submissionId }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1421,9 +1451,9 @@ export async function fetchEditHistory(itemId: string) {
|
|||||||
|
|
||||||
return data || [];
|
return data || [];
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logger.error('Error fetching edit history', {
|
handleNonCriticalError(error, {
|
||||||
itemId,
|
action: 'Fetch Edit History',
|
||||||
error: getErrorMessage(error),
|
metadata: { itemId }
|
||||||
});
|
});
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -1476,9 +1506,9 @@ export async function checkSubmissionConflict(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logger.error('Error checking submission conflict', {
|
handleNonCriticalError(error, {
|
||||||
submissionId,
|
action: 'Check Submission Conflict',
|
||||||
error: getErrorMessage(error),
|
metadata: { submissionId }
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -1526,9 +1556,9 @@ export async function fetchSubmissionVersions(
|
|||||||
|
|
||||||
return data || [];
|
return data || [];
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logger.error('Error fetching submission versions', {
|
handleNonCriticalError(error, {
|
||||||
submissionId,
|
action: 'Fetch Submission Versions',
|
||||||
error: getErrorMessage(error),
|
metadata: { submissionId }
|
||||||
});
|
});
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user