mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-28 01:27:05 -05:00
Compare commits
3 Commits
eccbe0ab1f
...
c52e538932
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c52e538932 | ||
|
|
48c1e9cdda | ||
|
|
2c9358e884 |
@@ -6345,7 +6345,8 @@ export type Database = {
|
|||||||
monitor_ban_attempts: { Args: never; Returns: undefined }
|
monitor_ban_attempts: { Args: never; Returns: undefined }
|
||||||
monitor_failed_submissions: { Args: never; Returns: undefined }
|
monitor_failed_submissions: { Args: never; Returns: undefined }
|
||||||
monitor_slow_approvals: { Args: never; Returns: undefined }
|
monitor_slow_approvals: { Args: never; Returns: undefined }
|
||||||
process_approval_transaction: {
|
process_approval_transaction:
|
||||||
|
| {
|
||||||
Args: {
|
Args: {
|
||||||
p_idempotency_key?: string
|
p_idempotency_key?: string
|
||||||
p_item_ids: string[]
|
p_item_ids: string[]
|
||||||
@@ -6356,6 +6357,21 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
Returns: Json
|
Returns: Json
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
Args: {
|
||||||
|
p_idempotency_key: string
|
||||||
|
p_item_ids: string[]
|
||||||
|
p_moderator_id: string
|
||||||
|
p_submission_id: string
|
||||||
|
}
|
||||||
|
Returns: {
|
||||||
|
approved_count: number
|
||||||
|
error_code: string
|
||||||
|
failed_items: Json
|
||||||
|
message: string
|
||||||
|
success: boolean
|
||||||
|
}[]
|
||||||
|
}
|
||||||
release_expired_locks: { Args: never; Returns: number }
|
release_expired_locks: { Args: never; Returns: number }
|
||||||
release_submission_lock: {
|
release_submission_lock: {
|
||||||
Args: { moderator_id: string; submission_id: string }
|
Args: { moderator_id: string; submission_id: string }
|
||||||
@@ -6455,7 +6471,8 @@ export type Database = {
|
|||||||
Args: { _action: string; _submission_id: string; _user_id: string }
|
Args: { _action: string; _submission_id: string; _user_id: string }
|
||||||
Returns: boolean
|
Returns: boolean
|
||||||
}
|
}
|
||||||
validate_submission_items_for_approval: {
|
validate_submission_items_for_approval:
|
||||||
|
| {
|
||||||
Args: { p_item_ids: string[] }
|
Args: { p_item_ids: string[] }
|
||||||
Returns: {
|
Returns: {
|
||||||
error_code: string
|
error_code: string
|
||||||
@@ -6465,6 +6482,15 @@ export type Database = {
|
|||||||
item_details: Json
|
item_details: Json
|
||||||
}[]
|
}[]
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
Args: { p_submission_id: string }
|
||||||
|
Returns: {
|
||||||
|
error_code: string
|
||||||
|
error_message: string
|
||||||
|
is_valid: boolean
|
||||||
|
item_details: Json
|
||||||
|
}[]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Enums: {
|
Enums: {
|
||||||
account_deletion_status:
|
account_deletion_status:
|
||||||
|
|||||||
@@ -1755,15 +1755,30 @@ export async function submitRideModelCreation(
|
|||||||
data: RideModelFormData,
|
data: RideModelFormData,
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<{ submitted: boolean; submissionId: string }> {
|
): Promise<{ submitted: boolean; submissionId: string }> {
|
||||||
|
// Rate limiting check
|
||||||
|
checkRateLimitOrThrow(userId, 'ride_model_creation');
|
||||||
|
recordSubmissionAttempt(userId);
|
||||||
|
|
||||||
|
// Breadcrumb tracking
|
||||||
|
breadcrumb.userAction('Start ride model submission', 'submitRideModelCreation', { userId });
|
||||||
|
|
||||||
// Validate required fields client-side
|
// Validate required fields client-side
|
||||||
assertValid(validateRideModelCreateFields(data));
|
assertValid(validateRideModelCreateFields(data));
|
||||||
|
|
||||||
// Check if user is banned
|
// Ban check with retry logic
|
||||||
|
const { withRetry } = await import('./retryHelpers');
|
||||||
|
breadcrumb.apiCall('profiles', 'SELECT');
|
||||||
|
const profile = await withRetry(
|
||||||
|
async () => {
|
||||||
const { data: profile } = await supabase
|
const { data: profile } = await supabase
|
||||||
.from('profiles')
|
.from('profiles')
|
||||||
.select('banned')
|
.select('banned')
|
||||||
.eq('user_id', userId)
|
.eq('user_id', userId)
|
||||||
.single();
|
.single();
|
||||||
|
return profile;
|
||||||
|
},
|
||||||
|
{ maxAttempts: 2 }
|
||||||
|
);
|
||||||
|
|
||||||
if (profile?.banned) {
|
if (profile?.banned) {
|
||||||
throw new Error('Account suspended. Contact support for assistance.');
|
throw new Error('Account suspended. Contact support for assistance.');
|
||||||
@@ -1786,6 +1801,10 @@ export async function submitRideModelCreation(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Submit with retry logic
|
||||||
|
breadcrumb.apiCall('content_submissions', 'INSERT');
|
||||||
|
const result = await withRetry(
|
||||||
|
async () => {
|
||||||
// Create the main submission record
|
// Create the main submission record
|
||||||
const { data: submissionData, error: submissionError } = await supabase
|
const { data: submissionData, error: submissionError } = await supabase
|
||||||
.from('content_submissions')
|
.from('content_submissions')
|
||||||
@@ -1868,6 +1887,28 @@ export async function submitRideModelCreation(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return { submitted: true, submissionId: submissionData.id };
|
return { submitted: true, submissionId: submissionData.id };
|
||||||
|
},
|
||||||
|
{
|
||||||
|
maxAttempts: 3,
|
||||||
|
onRetry: (attempt, error, delay) => {
|
||||||
|
logger.warn('Retrying ride model submission', { attempt, delay });
|
||||||
|
window.dispatchEvent(new CustomEvent('submission-retry', {
|
||||||
|
detail: { attempt, maxAttempts: 3, delay, type: 'ride_model' }
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
shouldRetry: (error) => {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
const message = error.message.toLowerCase();
|
||||||
|
if (message.includes('required')) return false;
|
||||||
|
if (message.includes('banned')) return false;
|
||||||
|
if (message.includes('slug')) return false;
|
||||||
|
}
|
||||||
|
return isRetryableError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1881,12 +1922,27 @@ export async function submitRideModelUpdate(
|
|||||||
data: RideModelFormData,
|
data: RideModelFormData,
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<{ submitted: boolean; submissionId: string }> {
|
): Promise<{ submitted: boolean; submissionId: string }> {
|
||||||
// Check if user is banned
|
// Rate limiting check
|
||||||
|
checkRateLimitOrThrow(userId, 'ride_model_update');
|
||||||
|
recordSubmissionAttempt(userId);
|
||||||
|
|
||||||
|
// Breadcrumb tracking
|
||||||
|
breadcrumb.userAction('Start ride model update', 'submitRideModelUpdate', { userId, rideModelId });
|
||||||
|
|
||||||
|
// Ban check with retry logic
|
||||||
|
const { withRetry } = await import('./retryHelpers');
|
||||||
|
breadcrumb.apiCall('profiles', 'SELECT');
|
||||||
|
const profile = await withRetry(
|
||||||
|
async () => {
|
||||||
const { data: profile } = await supabase
|
const { data: profile } = await supabase
|
||||||
.from('profiles')
|
.from('profiles')
|
||||||
.select('banned')
|
.select('banned')
|
||||||
.eq('user_id', userId)
|
.eq('user_id', userId)
|
||||||
.single();
|
.single();
|
||||||
|
return profile;
|
||||||
|
},
|
||||||
|
{ maxAttempts: 2 }
|
||||||
|
);
|
||||||
|
|
||||||
if (profile?.banned) {
|
if (profile?.banned) {
|
||||||
throw new Error('Account suspended. Contact support for assistance.');
|
throw new Error('Account suspended. Contact support for assistance.');
|
||||||
@@ -1909,6 +1965,10 @@ export async function submitRideModelUpdate(
|
|||||||
|
|
||||||
let processedImages = data.images;
|
let processedImages = data.images;
|
||||||
|
|
||||||
|
// Submit with retry logic
|
||||||
|
breadcrumb.apiCall('content_submissions', 'INSERT');
|
||||||
|
const result = await withRetry(
|
||||||
|
async () => {
|
||||||
// Create the main submission record
|
// Create the main submission record
|
||||||
const { data: submissionData, error: submissionError } = await supabase
|
const { data: submissionData, error: submissionError } = await supabase
|
||||||
.from('content_submissions')
|
.from('content_submissions')
|
||||||
@@ -1989,6 +2049,28 @@ export async function submitRideModelUpdate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return { submitted: true, submissionId: submissionData.id };
|
return { submitted: true, submissionId: submissionData.id };
|
||||||
|
},
|
||||||
|
{
|
||||||
|
maxAttempts: 3,
|
||||||
|
onRetry: (attempt, error, delay) => {
|
||||||
|
logger.warn('Retrying ride model update', { attempt, delay });
|
||||||
|
window.dispatchEvent(new CustomEvent('submission-retry', {
|
||||||
|
detail: { attempt, maxAttempts: 3, delay, type: 'ride_model_update' }
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
shouldRetry: (error) => {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
const message = error.message.toLowerCase();
|
||||||
|
if (message.includes('required')) return false;
|
||||||
|
if (message.includes('banned')) return false;
|
||||||
|
if (message.includes('slug')) return false;
|
||||||
|
}
|
||||||
|
return isRetryableError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2000,9 +2082,36 @@ export async function submitManufacturerCreation(
|
|||||||
data: CompanyFormData,
|
data: CompanyFormData,
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<{ submitted: boolean; submissionId: string }> {
|
): Promise<{ submitted: boolean; submissionId: string }> {
|
||||||
|
// Rate limiting check
|
||||||
|
checkRateLimitOrThrow(userId, 'manufacturer_creation');
|
||||||
|
recordSubmissionAttempt(userId);
|
||||||
|
|
||||||
|
// Breadcrumb tracking
|
||||||
|
breadcrumb.userAction('Start manufacturer submission', 'submitManufacturerCreation', { userId });
|
||||||
|
|
||||||
// Validate required fields client-side
|
// Validate required fields client-side
|
||||||
assertValid(validateCompanyCreateFields({ ...data, company_type: 'manufacturer' }));
|
assertValid(validateCompanyCreateFields({ ...data, company_type: 'manufacturer' }));
|
||||||
|
|
||||||
|
// Ban check with retry logic
|
||||||
|
const { withRetry } = await import('./retryHelpers');
|
||||||
|
breadcrumb.apiCall('profiles', 'SELECT');
|
||||||
|
const profile = await withRetry(
|
||||||
|
async () => {
|
||||||
|
const { data: profile } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('banned')
|
||||||
|
.eq('user_id', userId)
|
||||||
|
.single();
|
||||||
|
return profile;
|
||||||
|
},
|
||||||
|
{ maxAttempts: 2 }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (profile?.banned) {
|
||||||
|
throw new Error('Account suspended. Contact support for assistance.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload images
|
||||||
let processedImages = data.images;
|
let processedImages = data.images;
|
||||||
if (data.images?.uploaded && data.images.uploaded.length > 0) {
|
if (data.images?.uploaded && data.images.uploaded.length > 0) {
|
||||||
try {
|
try {
|
||||||
@@ -2016,6 +2125,10 @@ export async function submitManufacturerCreation(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Submit with retry logic
|
||||||
|
breadcrumb.apiCall('content_submissions', 'INSERT');
|
||||||
|
const result = await withRetry(
|
||||||
|
async () => {
|
||||||
const { data: submissionData, error: submissionError } = await supabase
|
const { data: submissionData, error: submissionError } = await supabase
|
||||||
.from('content_submissions')
|
.from('content_submissions')
|
||||||
.insert({
|
.insert({
|
||||||
@@ -2050,6 +2163,28 @@ export async function submitManufacturerCreation(
|
|||||||
if (itemError) throw itemError;
|
if (itemError) throw itemError;
|
||||||
|
|
||||||
return { submitted: true, submissionId: submissionData.id };
|
return { submitted: true, submissionId: submissionData.id };
|
||||||
|
},
|
||||||
|
{
|
||||||
|
maxAttempts: 3,
|
||||||
|
onRetry: (attempt, error, delay) => {
|
||||||
|
logger.warn('Retrying manufacturer submission', { attempt, delay });
|
||||||
|
window.dispatchEvent(new CustomEvent('submission-retry', {
|
||||||
|
detail: { attempt, maxAttempts: 3, delay, type: 'manufacturer' }
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
shouldRetry: (error) => {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
const message = error.message.toLowerCase();
|
||||||
|
if (message.includes('required')) return false;
|
||||||
|
if (message.includes('banned')) return false;
|
||||||
|
if (message.includes('slug')) return false;
|
||||||
|
}
|
||||||
|
return isRetryableError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function submitManufacturerUpdate(
|
export async function submitManufacturerUpdate(
|
||||||
@@ -2057,6 +2192,32 @@ export async function submitManufacturerUpdate(
|
|||||||
data: CompanyFormData,
|
data: CompanyFormData,
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<{ submitted: boolean; submissionId: string }> {
|
): Promise<{ submitted: boolean; submissionId: string }> {
|
||||||
|
// Rate limiting check
|
||||||
|
checkRateLimitOrThrow(userId, 'manufacturer_update');
|
||||||
|
recordSubmissionAttempt(userId);
|
||||||
|
|
||||||
|
// Breadcrumb tracking
|
||||||
|
breadcrumb.userAction('Start manufacturer update', 'submitManufacturerUpdate', { userId, companyId });
|
||||||
|
|
||||||
|
// Ban check with retry logic
|
||||||
|
const { withRetry } = await import('./retryHelpers');
|
||||||
|
breadcrumb.apiCall('profiles', 'SELECT');
|
||||||
|
const profile = await withRetry(
|
||||||
|
async () => {
|
||||||
|
const { data: profile } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('banned')
|
||||||
|
.eq('user_id', userId)
|
||||||
|
.single();
|
||||||
|
return profile;
|
||||||
|
},
|
||||||
|
{ maxAttempts: 2 }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (profile?.banned) {
|
||||||
|
throw new Error('Account suspended. Contact support for assistance.');
|
||||||
|
}
|
||||||
|
|
||||||
const { data: existingCompany, error: fetchError } = await supabase
|
const { data: existingCompany, error: fetchError } = await supabase
|
||||||
.from('companies')
|
.from('companies')
|
||||||
.select('*')
|
.select('*')
|
||||||
@@ -2073,6 +2234,10 @@ export async function submitManufacturerUpdate(
|
|||||||
|
|
||||||
let processedImages = data.images;
|
let processedImages = data.images;
|
||||||
|
|
||||||
|
// Submit with retry logic
|
||||||
|
breadcrumb.apiCall('content_submissions', 'INSERT');
|
||||||
|
const result = await withRetry(
|
||||||
|
async () => {
|
||||||
const { data: submissionData, error: submissionError } = await supabase
|
const { data: submissionData, error: submissionError } = await supabase
|
||||||
.from('content_submissions')
|
.from('content_submissions')
|
||||||
.insert({
|
.insert({
|
||||||
@@ -2105,15 +2270,64 @@ export async function submitManufacturerUpdate(
|
|||||||
if (itemError) throw itemError;
|
if (itemError) throw itemError;
|
||||||
|
|
||||||
return { submitted: true, submissionId: submissionData.id };
|
return { submitted: true, submissionId: submissionData.id };
|
||||||
|
},
|
||||||
|
{
|
||||||
|
maxAttempts: 3,
|
||||||
|
onRetry: (attempt, error, delay) => {
|
||||||
|
logger.warn('Retrying manufacturer update', { attempt, delay });
|
||||||
|
window.dispatchEvent(new CustomEvent('submission-retry', {
|
||||||
|
detail: { attempt, maxAttempts: 3, delay, type: 'manufacturer_update' }
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
shouldRetry: (error) => {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
const message = error.message.toLowerCase();
|
||||||
|
if (message.includes('required')) return false;
|
||||||
|
if (message.includes('banned')) return false;
|
||||||
|
if (message.includes('slug')) return false;
|
||||||
|
}
|
||||||
|
return isRetryableError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function submitDesignerCreation(
|
export async function submitDesignerCreation(
|
||||||
data: CompanyFormData,
|
data: CompanyFormData,
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<{ submitted: boolean; submissionId: string }> {
|
): Promise<{ submitted: boolean; submissionId: string }> {
|
||||||
|
// Rate limiting check
|
||||||
|
checkRateLimitOrThrow(userId, 'designer_creation');
|
||||||
|
recordSubmissionAttempt(userId);
|
||||||
|
|
||||||
|
// Breadcrumb tracking
|
||||||
|
breadcrumb.userAction('Start designer submission', 'submitDesignerCreation', { userId });
|
||||||
|
|
||||||
// Validate required fields client-side
|
// Validate required fields client-side
|
||||||
assertValid(validateCompanyCreateFields({ ...data, company_type: 'designer' }));
|
assertValid(validateCompanyCreateFields({ ...data, company_type: 'designer' }));
|
||||||
|
|
||||||
|
// Ban check with retry logic
|
||||||
|
const { withRetry } = await import('./retryHelpers');
|
||||||
|
breadcrumb.apiCall('profiles', 'SELECT');
|
||||||
|
const profile = await withRetry(
|
||||||
|
async () => {
|
||||||
|
const { data: profile } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('banned')
|
||||||
|
.eq('user_id', userId)
|
||||||
|
.single();
|
||||||
|
return profile;
|
||||||
|
},
|
||||||
|
{ maxAttempts: 2 }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (profile?.banned) {
|
||||||
|
throw new Error('Account suspended. Contact support for assistance.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload images
|
||||||
let processedImages = data.images;
|
let processedImages = data.images;
|
||||||
if (data.images?.uploaded && data.images.uploaded.length > 0) {
|
if (data.images?.uploaded && data.images.uploaded.length > 0) {
|
||||||
try {
|
try {
|
||||||
@@ -2127,6 +2341,10 @@ export async function submitDesignerCreation(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Submit with retry logic
|
||||||
|
breadcrumb.apiCall('content_submissions', 'INSERT');
|
||||||
|
const result = await withRetry(
|
||||||
|
async () => {
|
||||||
const { data: submissionData, error: submissionError } = await supabase
|
const { data: submissionData, error: submissionError } = await supabase
|
||||||
.from('content_submissions')
|
.from('content_submissions')
|
||||||
.insert({
|
.insert({
|
||||||
@@ -2161,6 +2379,28 @@ export async function submitDesignerCreation(
|
|||||||
if (itemError) throw itemError;
|
if (itemError) throw itemError;
|
||||||
|
|
||||||
return { submitted: true, submissionId: submissionData.id };
|
return { submitted: true, submissionId: submissionData.id };
|
||||||
|
},
|
||||||
|
{
|
||||||
|
maxAttempts: 3,
|
||||||
|
onRetry: (attempt, error, delay) => {
|
||||||
|
logger.warn('Retrying designer submission', { attempt, delay });
|
||||||
|
window.dispatchEvent(new CustomEvent('submission-retry', {
|
||||||
|
detail: { attempt, maxAttempts: 3, delay, type: 'designer' }
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
shouldRetry: (error) => {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
const message = error.message.toLowerCase();
|
||||||
|
if (message.includes('required')) return false;
|
||||||
|
if (message.includes('banned')) return false;
|
||||||
|
if (message.includes('slug')) return false;
|
||||||
|
}
|
||||||
|
return isRetryableError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function submitDesignerUpdate(
|
export async function submitDesignerUpdate(
|
||||||
@@ -2168,6 +2408,32 @@ export async function submitDesignerUpdate(
|
|||||||
data: CompanyFormData,
|
data: CompanyFormData,
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<{ submitted: boolean; submissionId: string }> {
|
): Promise<{ submitted: boolean; submissionId: string }> {
|
||||||
|
// Rate limiting check
|
||||||
|
checkRateLimitOrThrow(userId, 'designer_update');
|
||||||
|
recordSubmissionAttempt(userId);
|
||||||
|
|
||||||
|
// Breadcrumb tracking
|
||||||
|
breadcrumb.userAction('Start designer update', 'submitDesignerUpdate', { userId, companyId });
|
||||||
|
|
||||||
|
// Ban check with retry logic
|
||||||
|
const { withRetry } = await import('./retryHelpers');
|
||||||
|
breadcrumb.apiCall('profiles', 'SELECT');
|
||||||
|
const profile = await withRetry(
|
||||||
|
async () => {
|
||||||
|
const { data: profile } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('banned')
|
||||||
|
.eq('user_id', userId)
|
||||||
|
.single();
|
||||||
|
return profile;
|
||||||
|
},
|
||||||
|
{ maxAttempts: 2 }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (profile?.banned) {
|
||||||
|
throw new Error('Account suspended. Contact support for assistance.');
|
||||||
|
}
|
||||||
|
|
||||||
const { data: existingCompany, error: fetchError } = await supabase
|
const { data: existingCompany, error: fetchError } = await supabase
|
||||||
.from('companies')
|
.from('companies')
|
||||||
.select('*')
|
.select('*')
|
||||||
@@ -2184,6 +2450,10 @@ export async function submitDesignerUpdate(
|
|||||||
|
|
||||||
let processedImages = data.images;
|
let processedImages = data.images;
|
||||||
|
|
||||||
|
// Submit with retry logic
|
||||||
|
breadcrumb.apiCall('content_submissions', 'INSERT');
|
||||||
|
const result = await withRetry(
|
||||||
|
async () => {
|
||||||
const { data: submissionData, error: submissionError } = await supabase
|
const { data: submissionData, error: submissionError } = await supabase
|
||||||
.from('content_submissions')
|
.from('content_submissions')
|
||||||
.insert({
|
.insert({
|
||||||
@@ -2216,15 +2486,64 @@ export async function submitDesignerUpdate(
|
|||||||
if (itemError) throw itemError;
|
if (itemError) throw itemError;
|
||||||
|
|
||||||
return { submitted: true, submissionId: submissionData.id };
|
return { submitted: true, submissionId: submissionData.id };
|
||||||
|
},
|
||||||
|
{
|
||||||
|
maxAttempts: 3,
|
||||||
|
onRetry: (attempt, error, delay) => {
|
||||||
|
logger.warn('Retrying designer update', { attempt, delay });
|
||||||
|
window.dispatchEvent(new CustomEvent('submission-retry', {
|
||||||
|
detail: { attempt, maxAttempts: 3, delay, type: 'designer_update' }
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
shouldRetry: (error) => {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
const message = error.message.toLowerCase();
|
||||||
|
if (message.includes('required')) return false;
|
||||||
|
if (message.includes('banned')) return false;
|
||||||
|
if (message.includes('slug')) return false;
|
||||||
|
}
|
||||||
|
return isRetryableError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function submitOperatorCreation(
|
export async function submitOperatorCreation(
|
||||||
data: CompanyFormData,
|
data: CompanyFormData,
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<{ submitted: boolean; submissionId: string }> {
|
): Promise<{ submitted: boolean; submissionId: string }> {
|
||||||
|
// Rate limiting check
|
||||||
|
checkRateLimitOrThrow(userId, 'operator_creation');
|
||||||
|
recordSubmissionAttempt(userId);
|
||||||
|
|
||||||
|
// Breadcrumb tracking
|
||||||
|
breadcrumb.userAction('Start operator submission', 'submitOperatorCreation', { userId });
|
||||||
|
|
||||||
// Validate required fields client-side
|
// Validate required fields client-side
|
||||||
assertValid(validateCompanyCreateFields({ ...data, company_type: 'operator' }));
|
assertValid(validateCompanyCreateFields({ ...data, company_type: 'operator' }));
|
||||||
|
|
||||||
|
// Ban check with retry logic
|
||||||
|
const { withRetry } = await import('./retryHelpers');
|
||||||
|
breadcrumb.apiCall('profiles', 'SELECT');
|
||||||
|
const profile = await withRetry(
|
||||||
|
async () => {
|
||||||
|
const { data: profile } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('banned')
|
||||||
|
.eq('user_id', userId)
|
||||||
|
.single();
|
||||||
|
return profile;
|
||||||
|
},
|
||||||
|
{ maxAttempts: 2 }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (profile?.banned) {
|
||||||
|
throw new Error('Account suspended. Contact support for assistance.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload images
|
||||||
let processedImages = data.images;
|
let processedImages = data.images;
|
||||||
if (data.images?.uploaded && data.images.uploaded.length > 0) {
|
if (data.images?.uploaded && data.images.uploaded.length > 0) {
|
||||||
try {
|
try {
|
||||||
@@ -2238,6 +2557,10 @@ export async function submitOperatorCreation(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Submit with retry logic
|
||||||
|
breadcrumb.apiCall('content_submissions', 'INSERT');
|
||||||
|
const result = await withRetry(
|
||||||
|
async () => {
|
||||||
const { data: submissionData, error: submissionError } = await supabase
|
const { data: submissionData, error: submissionError } = await supabase
|
||||||
.from('content_submissions')
|
.from('content_submissions')
|
||||||
.insert({
|
.insert({
|
||||||
@@ -2272,6 +2595,28 @@ export async function submitOperatorCreation(
|
|||||||
if (itemError) throw itemError;
|
if (itemError) throw itemError;
|
||||||
|
|
||||||
return { submitted: true, submissionId: submissionData.id };
|
return { submitted: true, submissionId: submissionData.id };
|
||||||
|
},
|
||||||
|
{
|
||||||
|
maxAttempts: 3,
|
||||||
|
onRetry: (attempt, error, delay) => {
|
||||||
|
logger.warn('Retrying operator submission', { attempt, delay });
|
||||||
|
window.dispatchEvent(new CustomEvent('submission-retry', {
|
||||||
|
detail: { attempt, maxAttempts: 3, delay, type: 'operator' }
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
shouldRetry: (error) => {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
const message = error.message.toLowerCase();
|
||||||
|
if (message.includes('required')) return false;
|
||||||
|
if (message.includes('banned')) return false;
|
||||||
|
if (message.includes('slug')) return false;
|
||||||
|
}
|
||||||
|
return isRetryableError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function submitOperatorUpdate(
|
export async function submitOperatorUpdate(
|
||||||
@@ -2279,6 +2624,32 @@ export async function submitOperatorUpdate(
|
|||||||
data: CompanyFormData,
|
data: CompanyFormData,
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<{ submitted: boolean; submissionId: string }> {
|
): Promise<{ submitted: boolean; submissionId: string }> {
|
||||||
|
// Rate limiting check
|
||||||
|
checkRateLimitOrThrow(userId, 'operator_update');
|
||||||
|
recordSubmissionAttempt(userId);
|
||||||
|
|
||||||
|
// Breadcrumb tracking
|
||||||
|
breadcrumb.userAction('Start operator update', 'submitOperatorUpdate', { userId, companyId });
|
||||||
|
|
||||||
|
// Ban check with retry logic
|
||||||
|
const { withRetry } = await import('./retryHelpers');
|
||||||
|
breadcrumb.apiCall('profiles', 'SELECT');
|
||||||
|
const profile = await withRetry(
|
||||||
|
async () => {
|
||||||
|
const { data: profile } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('banned')
|
||||||
|
.eq('user_id', userId)
|
||||||
|
.single();
|
||||||
|
return profile;
|
||||||
|
},
|
||||||
|
{ maxAttempts: 2 }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (profile?.banned) {
|
||||||
|
throw new Error('Account suspended. Contact support for assistance.');
|
||||||
|
}
|
||||||
|
|
||||||
const { data: existingCompany, error: fetchError } = await supabase
|
const { data: existingCompany, error: fetchError } = await supabase
|
||||||
.from('companies')
|
.from('companies')
|
||||||
.select('*')
|
.select('*')
|
||||||
@@ -2295,6 +2666,10 @@ export async function submitOperatorUpdate(
|
|||||||
|
|
||||||
let processedImages = data.images;
|
let processedImages = data.images;
|
||||||
|
|
||||||
|
// Submit with retry logic
|
||||||
|
breadcrumb.apiCall('content_submissions', 'INSERT');
|
||||||
|
const result = await withRetry(
|
||||||
|
async () => {
|
||||||
const { data: submissionData, error: submissionError } = await supabase
|
const { data: submissionData, error: submissionError } = await supabase
|
||||||
.from('content_submissions')
|
.from('content_submissions')
|
||||||
.insert({
|
.insert({
|
||||||
@@ -2327,15 +2702,64 @@ export async function submitOperatorUpdate(
|
|||||||
if (itemError) throw itemError;
|
if (itemError) throw itemError;
|
||||||
|
|
||||||
return { submitted: true, submissionId: submissionData.id };
|
return { submitted: true, submissionId: submissionData.id };
|
||||||
|
},
|
||||||
|
{
|
||||||
|
maxAttempts: 3,
|
||||||
|
onRetry: (attempt, error, delay) => {
|
||||||
|
logger.warn('Retrying operator update', { attempt, delay });
|
||||||
|
window.dispatchEvent(new CustomEvent('submission-retry', {
|
||||||
|
detail: { attempt, maxAttempts: 3, delay, type: 'operator_update' }
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
shouldRetry: (error) => {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
const message = error.message.toLowerCase();
|
||||||
|
if (message.includes('required')) return false;
|
||||||
|
if (message.includes('banned')) return false;
|
||||||
|
if (message.includes('slug')) return false;
|
||||||
|
}
|
||||||
|
return isRetryableError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function submitPropertyOwnerCreation(
|
export async function submitPropertyOwnerCreation(
|
||||||
data: CompanyFormData,
|
data: CompanyFormData,
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<{ submitted: boolean; submissionId: string }> {
|
): Promise<{ submitted: boolean; submissionId: string }> {
|
||||||
|
// Rate limiting check
|
||||||
|
checkRateLimitOrThrow(userId, 'property_owner_creation');
|
||||||
|
recordSubmissionAttempt(userId);
|
||||||
|
|
||||||
|
// Breadcrumb tracking
|
||||||
|
breadcrumb.userAction('Start property owner submission', 'submitPropertyOwnerCreation', { userId });
|
||||||
|
|
||||||
// Validate required fields client-side
|
// Validate required fields client-side
|
||||||
assertValid(validateCompanyCreateFields({ ...data, company_type: 'property_owner' }));
|
assertValid(validateCompanyCreateFields({ ...data, company_type: 'property_owner' }));
|
||||||
|
|
||||||
|
// Ban check with retry logic
|
||||||
|
const { withRetry } = await import('./retryHelpers');
|
||||||
|
breadcrumb.apiCall('profiles', 'SELECT');
|
||||||
|
const profile = await withRetry(
|
||||||
|
async () => {
|
||||||
|
const { data: profile } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('banned')
|
||||||
|
.eq('user_id', userId)
|
||||||
|
.single();
|
||||||
|
return profile;
|
||||||
|
},
|
||||||
|
{ maxAttempts: 2 }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (profile?.banned) {
|
||||||
|
throw new Error('Account suspended. Contact support for assistance.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload images
|
||||||
let processedImages = data.images;
|
let processedImages = data.images;
|
||||||
if (data.images?.uploaded && data.images.uploaded.length > 0) {
|
if (data.images?.uploaded && data.images.uploaded.length > 0) {
|
||||||
try {
|
try {
|
||||||
@@ -2349,6 +2773,10 @@ export async function submitPropertyOwnerCreation(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Submit with retry logic
|
||||||
|
breadcrumb.apiCall('content_submissions', 'INSERT');
|
||||||
|
const result = await withRetry(
|
||||||
|
async () => {
|
||||||
const { data: submissionData, error: submissionError } = await supabase
|
const { data: submissionData, error: submissionError } = await supabase
|
||||||
.from('content_submissions')
|
.from('content_submissions')
|
||||||
.insert({
|
.insert({
|
||||||
@@ -2383,6 +2811,28 @@ export async function submitPropertyOwnerCreation(
|
|||||||
if (itemError) throw itemError;
|
if (itemError) throw itemError;
|
||||||
|
|
||||||
return { submitted: true, submissionId: submissionData.id };
|
return { submitted: true, submissionId: submissionData.id };
|
||||||
|
},
|
||||||
|
{
|
||||||
|
maxAttempts: 3,
|
||||||
|
onRetry: (attempt, error, delay) => {
|
||||||
|
logger.warn('Retrying property owner submission', { attempt, delay });
|
||||||
|
window.dispatchEvent(new CustomEvent('submission-retry', {
|
||||||
|
detail: { attempt, maxAttempts: 3, delay, type: 'property_owner' }
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
shouldRetry: (error) => {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
const message = error.message.toLowerCase();
|
||||||
|
if (message.includes('required')) return false;
|
||||||
|
if (message.includes('banned')) return false;
|
||||||
|
if (message.includes('slug')) return false;
|
||||||
|
}
|
||||||
|
return isRetryableError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function submitPropertyOwnerUpdate(
|
export async function submitPropertyOwnerUpdate(
|
||||||
@@ -2390,6 +2840,32 @@ export async function submitPropertyOwnerUpdate(
|
|||||||
data: CompanyFormData,
|
data: CompanyFormData,
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<{ submitted: boolean; submissionId: string }> {
|
): Promise<{ submitted: boolean; submissionId: string }> {
|
||||||
|
// Rate limiting check
|
||||||
|
checkRateLimitOrThrow(userId, 'property_owner_update');
|
||||||
|
recordSubmissionAttempt(userId);
|
||||||
|
|
||||||
|
// Breadcrumb tracking
|
||||||
|
breadcrumb.userAction('Start property owner update', 'submitPropertyOwnerUpdate', { userId, companyId });
|
||||||
|
|
||||||
|
// Ban check with retry logic
|
||||||
|
const { withRetry } = await import('./retryHelpers');
|
||||||
|
breadcrumb.apiCall('profiles', 'SELECT');
|
||||||
|
const profile = await withRetry(
|
||||||
|
async () => {
|
||||||
|
const { data: profile } = await supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('banned')
|
||||||
|
.eq('user_id', userId)
|
||||||
|
.single();
|
||||||
|
return profile;
|
||||||
|
},
|
||||||
|
{ maxAttempts: 2 }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (profile?.banned) {
|
||||||
|
throw new Error('Account suspended. Contact support for assistance.');
|
||||||
|
}
|
||||||
|
|
||||||
const { data: existingCompany, error: fetchError } = await supabase
|
const { data: existingCompany, error: fetchError } = await supabase
|
||||||
.from('companies')
|
.from('companies')
|
||||||
.select('*')
|
.select('*')
|
||||||
@@ -2406,6 +2882,10 @@ export async function submitPropertyOwnerUpdate(
|
|||||||
|
|
||||||
let processedImages = data.images;
|
let processedImages = data.images;
|
||||||
|
|
||||||
|
// Submit with retry logic
|
||||||
|
breadcrumb.apiCall('content_submissions', 'INSERT');
|
||||||
|
const result = await withRetry(
|
||||||
|
async () => {
|
||||||
const { data: submissionData, error: submissionError } = await supabase
|
const { data: submissionData, error: submissionError } = await supabase
|
||||||
.from('content_submissions')
|
.from('content_submissions')
|
||||||
.insert({
|
.insert({
|
||||||
@@ -2438,6 +2918,28 @@ export async function submitPropertyOwnerUpdate(
|
|||||||
if (itemError) throw itemError;
|
if (itemError) throw itemError;
|
||||||
|
|
||||||
return { submitted: true, submissionId: submissionData.id };
|
return { submitted: true, submissionId: submissionData.id };
|
||||||
|
},
|
||||||
|
{
|
||||||
|
maxAttempts: 3,
|
||||||
|
onRetry: (attempt, error, delay) => {
|
||||||
|
logger.warn('Retrying property owner update', { attempt, delay });
|
||||||
|
window.dispatchEvent(new CustomEvent('submission-retry', {
|
||||||
|
detail: { attempt, maxAttempts: 3, delay, type: 'property_owner_update' }
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
shouldRetry: (error) => {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
const message = error.message.toLowerCase();
|
||||||
|
if (message.includes('required')) return false;
|
||||||
|
if (message.includes('banned')) return false;
|
||||||
|
if (message.includes('slug')) return false;
|
||||||
|
}
|
||||||
|
return isRetryableError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,312 @@
|
|||||||
|
-- Drop old validation function
|
||||||
|
DROP FUNCTION IF EXISTS public.validate_submission_items_for_approval(uuid);
|
||||||
|
|
||||||
|
-- Create enhanced validation function with error codes and item details
|
||||||
|
CREATE OR REPLACE FUNCTION public.validate_submission_items_for_approval(
|
||||||
|
p_submission_id UUID
|
||||||
|
)
|
||||||
|
RETURNS TABLE(
|
||||||
|
is_valid BOOLEAN,
|
||||||
|
error_message TEXT,
|
||||||
|
error_code TEXT,
|
||||||
|
item_details JSONB
|
||||||
|
)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path TO 'public'
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_item RECORD;
|
||||||
|
v_error_msg TEXT;
|
||||||
|
v_error_code TEXT;
|
||||||
|
v_item_details JSONB;
|
||||||
|
BEGIN
|
||||||
|
-- Validate each submission item
|
||||||
|
FOR v_item IN
|
||||||
|
SELECT
|
||||||
|
si.id,
|
||||||
|
si.item_type,
|
||||||
|
si.action_type,
|
||||||
|
si.park_submission_id,
|
||||||
|
si.ride_submission_id,
|
||||||
|
si.company_submission_id,
|
||||||
|
si.ride_model_submission_id,
|
||||||
|
si.photo_submission_id,
|
||||||
|
si.timeline_event_submission_id
|
||||||
|
FROM submission_items si
|
||||||
|
WHERE si.submission_id = p_submission_id
|
||||||
|
ORDER BY si.order_index
|
||||||
|
LOOP
|
||||||
|
-- Build item details for error reporting
|
||||||
|
v_item_details := jsonb_build_object(
|
||||||
|
'item_id', v_item.id,
|
||||||
|
'item_type', v_item.item_type,
|
||||||
|
'action_type', v_item.action_type
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Validate based on item type
|
||||||
|
IF v_item.item_type = 'park' THEN
|
||||||
|
-- Validate park submission
|
||||||
|
IF v_item.park_submission_id IS NULL THEN
|
||||||
|
RETURN QUERY SELECT FALSE, 'Park submission data missing'::TEXT, '23502'::TEXT, v_item_details;
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Get park details for error reporting
|
||||||
|
SELECT v_item_details || jsonb_build_object('name', ps.name, 'slug', ps.slug)
|
||||||
|
INTO v_item_details
|
||||||
|
FROM park_submissions ps
|
||||||
|
WHERE ps.id = v_item.park_submission_id;
|
||||||
|
|
||||||
|
-- Check for duplicate slugs
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM parks p
|
||||||
|
WHERE p.slug = (SELECT slug FROM park_submissions WHERE id = v_item.park_submission_id)
|
||||||
|
AND v_item.action_type = 'create'
|
||||||
|
) THEN
|
||||||
|
RETURN QUERY SELECT FALSE, 'Park slug already exists'::TEXT, '23505'::TEXT, v_item_details;
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
ELSIF v_item.item_type = 'ride' THEN
|
||||||
|
-- Validate ride submission
|
||||||
|
IF v_item.ride_submission_id IS NULL THEN
|
||||||
|
RETURN QUERY SELECT FALSE, 'Ride submission data missing'::TEXT, '23502'::TEXT, v_item_details;
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Get ride details for error reporting
|
||||||
|
SELECT v_item_details || jsonb_build_object('name', rs.name, 'slug', rs.slug)
|
||||||
|
INTO v_item_details
|
||||||
|
FROM ride_submissions rs
|
||||||
|
WHERE rs.id = v_item.ride_submission_id;
|
||||||
|
|
||||||
|
-- Check for duplicate slugs within same park
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM rides r
|
||||||
|
WHERE r.slug = (SELECT slug FROM ride_submissions WHERE id = v_item.ride_submission_id)
|
||||||
|
AND r.park_id = (SELECT park_id FROM ride_submissions WHERE id = v_item.ride_submission_id)
|
||||||
|
AND v_item.action_type = 'create'
|
||||||
|
) THEN
|
||||||
|
RETURN QUERY SELECT FALSE, 'Ride slug already exists in this park'::TEXT, '23505'::TEXT, v_item_details;
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
ELSIF v_item.item_type IN ('manufacturer', 'operator', 'designer', 'property_owner') THEN
|
||||||
|
-- Validate company submission
|
||||||
|
IF v_item.company_submission_id IS NULL THEN
|
||||||
|
RETURN QUERY SELECT FALSE, 'Company submission data missing'::TEXT, '23502'::TEXT, v_item_details;
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Get company details for error reporting
|
||||||
|
SELECT v_item_details || jsonb_build_object('name', cs.name, 'slug', cs.slug)
|
||||||
|
INTO v_item_details
|
||||||
|
FROM company_submissions cs
|
||||||
|
WHERE cs.id = v_item.company_submission_id;
|
||||||
|
|
||||||
|
-- Check for duplicate slugs
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM companies c
|
||||||
|
WHERE c.slug = (SELECT slug FROM company_submissions WHERE id = v_item.company_submission_id)
|
||||||
|
AND v_item.action_type = 'create'
|
||||||
|
) THEN
|
||||||
|
RETURN QUERY SELECT FALSE, 'Company slug already exists'::TEXT, '23505'::TEXT, v_item_details;
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
ELSIF v_item.item_type = 'ride_model' THEN
|
||||||
|
-- Validate ride model submission
|
||||||
|
IF v_item.ride_model_submission_id IS NULL THEN
|
||||||
|
RETURN QUERY SELECT FALSE, 'Ride model submission data missing'::TEXT, '23502'::TEXT, v_item_details;
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Get ride model details for error reporting
|
||||||
|
SELECT v_item_details || jsonb_build_object('name', rms.name, 'slug', rms.slug)
|
||||||
|
INTO v_item_details
|
||||||
|
FROM ride_model_submissions rms
|
||||||
|
WHERE rms.id = v_item.ride_model_submission_id;
|
||||||
|
|
||||||
|
-- Check for duplicate slugs
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1 FROM ride_models rm
|
||||||
|
WHERE rm.slug = (SELECT slug FROM ride_model_submissions WHERE id = v_item.ride_model_submission_id)
|
||||||
|
AND v_item.action_type = 'create'
|
||||||
|
) THEN
|
||||||
|
RETURN QUERY SELECT FALSE, 'Ride model slug already exists'::TEXT, '23505'::TEXT, v_item_details;
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
ELSIF v_item.item_type = 'photo' THEN
|
||||||
|
-- Validate photo submission
|
||||||
|
IF v_item.photo_submission_id IS NULL THEN
|
||||||
|
RETURN QUERY SELECT FALSE, 'Photo submission data missing'::TEXT, '23502'::TEXT, v_item_details;
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
ELSIF v_item.item_type = 'timeline_event' THEN
|
||||||
|
-- Validate timeline event submission
|
||||||
|
IF v_item.timeline_event_submission_id IS NULL THEN
|
||||||
|
RETURN QUERY SELECT FALSE, 'Timeline event submission data missing'::TEXT, '23502'::TEXT, v_item_details;
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
ELSE
|
||||||
|
-- Unknown item type
|
||||||
|
RETURN QUERY SELECT FALSE, 'Unknown item type: ' || v_item.item_type::TEXT, '22023'::TEXT, v_item_details;
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
-- All validations passed
|
||||||
|
RETURN QUERY SELECT TRUE, NULL::TEXT, NULL::TEXT, NULL::JSONB;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- Update process_approval_transaction to use enhanced validation
|
||||||
|
CREATE OR REPLACE FUNCTION public.process_approval_transaction(
|
||||||
|
p_submission_id UUID,
|
||||||
|
p_item_ids UUID[],
|
||||||
|
p_moderator_id UUID,
|
||||||
|
p_idempotency_key TEXT
|
||||||
|
)
|
||||||
|
RETURNS TABLE(
|
||||||
|
success BOOLEAN,
|
||||||
|
message TEXT,
|
||||||
|
error_code TEXT,
|
||||||
|
approved_count INTEGER,
|
||||||
|
failed_items JSONB
|
||||||
|
)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path TO 'public'
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
v_start_time TIMESTAMPTZ := clock_timestamp();
|
||||||
|
v_validation_result RECORD;
|
||||||
|
v_approved_count INTEGER := 0;
|
||||||
|
v_failed_items JSONB := '[]'::JSONB;
|
||||||
|
v_submission_status TEXT;
|
||||||
|
v_error_code TEXT;
|
||||||
|
BEGIN
|
||||||
|
-- Validate moderator permission
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM user_roles
|
||||||
|
WHERE user_id = p_moderator_id
|
||||||
|
AND role IN ('moderator', 'admin', 'superuser')
|
||||||
|
) THEN
|
||||||
|
-- Log failure
|
||||||
|
INSERT INTO approval_transaction_metrics (
|
||||||
|
submission_id, moderator_id, idempotency_key, item_count,
|
||||||
|
approved_count, failed_count, duration_ms, error_code, error_details
|
||||||
|
) VALUES (
|
||||||
|
p_submission_id, p_moderator_id, p_idempotency_key, array_length(p_item_ids, 1),
|
||||||
|
0, array_length(p_item_ids, 1),
|
||||||
|
EXTRACT(EPOCH FROM (clock_timestamp() - v_start_time)) * 1000,
|
||||||
|
'UNAUTHORIZED',
|
||||||
|
jsonb_build_object('message', 'User does not have moderation privileges')
|
||||||
|
);
|
||||||
|
|
||||||
|
RETURN QUERY SELECT FALSE, 'Unauthorized: User does not have moderation privileges'::TEXT, 'UNAUTHORIZED'::TEXT, 0, '[]'::JSONB;
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Run enhanced validation with error codes
|
||||||
|
SELECT * INTO v_validation_result
|
||||||
|
FROM validate_submission_items_for_approval(p_submission_id)
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF NOT v_validation_result.is_valid THEN
|
||||||
|
-- Log validation failure with detailed error info
|
||||||
|
INSERT INTO approval_transaction_metrics (
|
||||||
|
submission_id, moderator_id, idempotency_key, item_count,
|
||||||
|
approved_count, failed_count, duration_ms, error_code, error_details
|
||||||
|
) VALUES (
|
||||||
|
p_submission_id, p_moderator_id, p_idempotency_key, array_length(p_item_ids, 1),
|
||||||
|
0, array_length(p_item_ids, 1),
|
||||||
|
EXTRACT(EPOCH FROM (clock_timestamp() - v_start_time)) * 1000,
|
||||||
|
v_validation_result.error_code,
|
||||||
|
jsonb_build_object(
|
||||||
|
'message', v_validation_result.error_message,
|
||||||
|
'item_details', v_validation_result.item_details
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
RETURN QUERY SELECT
|
||||||
|
FALSE,
|
||||||
|
v_validation_result.error_message::TEXT,
|
||||||
|
v_validation_result.error_code::TEXT,
|
||||||
|
0,
|
||||||
|
jsonb_build_array(v_validation_result.item_details);
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Process approvals for each item
|
||||||
|
DECLARE
|
||||||
|
v_item_id UUID;
|
||||||
|
v_item RECORD;
|
||||||
|
BEGIN
|
||||||
|
FOREACH v_item_id IN ARRAY p_item_ids
|
||||||
|
LOOP
|
||||||
|
BEGIN
|
||||||
|
-- Get item details
|
||||||
|
SELECT * INTO v_item
|
||||||
|
FROM submission_items
|
||||||
|
WHERE id = v_item_id;
|
||||||
|
|
||||||
|
-- Approve the item (implementation depends on item type)
|
||||||
|
UPDATE submission_items
|
||||||
|
SET status = 'approved', updated_at = NOW()
|
||||||
|
WHERE id = v_item_id;
|
||||||
|
|
||||||
|
v_approved_count := v_approved_count + 1;
|
||||||
|
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
-- Capture failed item with error details
|
||||||
|
v_failed_items := v_failed_items || jsonb_build_object(
|
||||||
|
'item_id', v_item_id,
|
||||||
|
'error', SQLERRM,
|
||||||
|
'error_code', SQLSTATE
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
END LOOP;
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- Determine final submission status
|
||||||
|
IF v_approved_count = array_length(p_item_ids, 1) THEN
|
||||||
|
v_submission_status := 'approved';
|
||||||
|
ELSIF v_approved_count > 0 THEN
|
||||||
|
v_submission_status := 'partially_approved';
|
||||||
|
ELSE
|
||||||
|
v_submission_status := 'rejected';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Update submission status
|
||||||
|
UPDATE content_submissions
|
||||||
|
SET
|
||||||
|
status = v_submission_status,
|
||||||
|
reviewed_at = NOW(),
|
||||||
|
reviewer_id = p_moderator_id
|
||||||
|
WHERE id = p_submission_id;
|
||||||
|
|
||||||
|
-- Log success metrics
|
||||||
|
INSERT INTO approval_transaction_metrics (
|
||||||
|
submission_id, moderator_id, idempotency_key, item_count,
|
||||||
|
approved_count, failed_count, duration_ms, error_code, error_details
|
||||||
|
) VALUES (
|
||||||
|
p_submission_id, p_moderator_id, p_idempotency_key, array_length(p_item_ids, 1),
|
||||||
|
v_approved_count, array_length(p_item_ids, 1) - v_approved_count,
|
||||||
|
EXTRACT(EPOCH FROM (clock_timestamp() - v_start_time)) * 1000,
|
||||||
|
NULL,
|
||||||
|
CASE WHEN jsonb_array_length(v_failed_items) > 0 THEN v_failed_items ELSE NULL END
|
||||||
|
);
|
||||||
|
|
||||||
|
RETURN QUERY SELECT
|
||||||
|
TRUE,
|
||||||
|
format('Approved %s of %s items', v_approved_count, array_length(p_item_ids, 1))::TEXT,
|
||||||
|
NULL::TEXT,
|
||||||
|
v_approved_count,
|
||||||
|
v_failed_items;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
Reference in New Issue
Block a user