Files
thrilltrack-explorer/src-old/lib/conflictResolutionService.ts

300 lines
7.9 KiB
TypeScript

import { supabase } from '@/lib/supabaseClient';
import { handleError, handleNonCriticalError } from '@/lib/errorHandler';
import { updateSubmissionItem, type SubmissionItemWithDeps, type DependencyConflict } from './submissionItemsService';
export interface ResolutionResult {
success: boolean;
updatedSelections?: Set<string>;
error?: string;
}
/**
* Main conflict resolution processor
*/
export async function resolveConflicts(
conflicts: DependencyConflict[],
resolutions: Record<string, string>,
items: SubmissionItemWithDeps[],
userId: string
): Promise<ResolutionResult> {
try {
const updatedSelections = new Set<string>();
for (const conflict of conflicts) {
const resolution = resolutions[conflict.itemId];
if (!resolution) {
return {
success: false,
error: `No resolution selected for ${conflict.itemId}`,
};
}
const suggestion = conflict.suggestions.find(s => s.action === resolution);
if (!suggestion) {
return {
success: false,
error: `Invalid resolution action: ${resolution}`,
};
}
// Process each resolution action
switch (suggestion.action) {
case 'link_existing':
if (!suggestion.entityId) {
return {
success: false,
error: 'Entity ID required for link_existing action',
};
}
await linkToExistingEntity(conflict.itemId, suggestion.entityId);
updatedSelections.add(conflict.itemId);
break;
case 'create_parent':
const item = items.find(i => i.id === conflict.itemId);
if (item?.depends_on) {
updatedSelections.add(item.depends_on);
updatedSelections.add(conflict.itemId);
}
break;
case 'cascade_reject':
await cascadeRejectDependents(conflict.itemId, items);
break;
case 'escalate':
const submissionId = items[0]?.submission_id;
if (submissionId) {
await escalateForAdminReview(submissionId, `Conflict resolution needed: ${conflict.message}`, userId);
}
return {
success: true,
updatedSelections: new Set(),
};
default:
return {
success: false,
error: `Unknown action: ${suggestion.action}`,
};
}
}
return {
success: true,
updatedSelections,
};
} catch (error: unknown) {
handleError(error, {
action: 'Resolve conflicts',
metadata: { conflictCount: conflicts.length },
});
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
/**
* Link submission item to existing database entity
*/
async function linkToExistingEntity(itemId: string, entityId: string): Promise<void> {
await updateSubmissionItem(itemId, {
approved_entity_id: entityId,
status: 'approved' as const,
});
}
/**
* Cascade reject all dependent items
*/
async function cascadeRejectDependents(
itemId: string,
items: SubmissionItemWithDeps[]
): Promise<void> {
const item = items.find(i => i.id === itemId);
if (!item?.dependents) return;
const toReject: string[] = [];
function collectDependents(current: SubmissionItemWithDeps) {
if (current.dependents) {
for (const dep of current.dependents) {
toReject.push(dep.id);
collectDependents(dep);
}
}
}
collectDependents(item);
// Reject all collected dependents
for (const depId of toReject) {
await updateSubmissionItem(depId, {
status: 'rejected' as const,
rejection_reason: 'Parent dependency was rejected',
});
}
}
/**
* Escalate submission for admin review
*/
async function escalateForAdminReview(
submissionId: string,
reason: string,
userId: string
): Promise<void> {
const { error } = await supabase
.from('content_submissions')
.update({
status: 'pending' as const,
escalation_reason: reason,
escalated_by: userId,
updated_at: new Date().toISOString(),
})
.eq('id', submissionId);
if (error) {
throw new Error(`Failed to escalate submission: ${error.message}`);
}
}
/**
* Find existing entities that match submission data
*/
export async function findMatchingEntities(
itemType: string,
itemData: any
): Promise<Array<{ id: string; name: string; similarity: number }>> {
const tableName = getTableNameForItemType(itemType);
if (!tableName) return [];
try {
// Query based on table type
if (tableName === 'companies') {
const { data, error } = await supabase
.from('companies')
.select('id, name')
.ilike('name', `%${itemData.name}%`)
.limit(5);
if (error) throw error;
return (data || []).map(entity => ({
id: entity.id,
name: entity.name,
similarity: calculateSimilarity(itemData.name, entity.name),
})).sort((a, b) => b.similarity - a.similarity);
} else if (tableName === 'parks') {
const { data, error } = await supabase
.from('parks')
.select('id, name')
.ilike('name', `%${itemData.name}%`)
.limit(5);
if (error) throw error;
return (data || []).map(entity => ({
id: entity.id,
name: entity.name,
similarity: calculateSimilarity(itemData.name, entity.name),
})).sort((a, b) => b.similarity - a.similarity);
} else if (tableName === 'rides') {
const { data, error } = await supabase
.from('rides')
.select('id, name')
.ilike('name', `%${itemData.name}%`)
.limit(5);
if (error) throw error;
return (data || []).map(entity => ({
id: entity.id,
name: entity.name,
similarity: calculateSimilarity(itemData.name, entity.name),
})).sort((a, b) => b.similarity - a.similarity);
} else if (tableName === 'ride_models') {
const { data, error } = await supabase
.from('ride_models')
.select('id, name')
.ilike('name', `%${itemData.name}%`)
.limit(5);
if (error) throw error;
return (data || []).map(entity => ({
id: entity.id,
name: entity.name,
similarity: calculateSimilarity(itemData.name, entity.name),
})).sort((a, b) => b.similarity - a.similarity);
}
return [];
} catch (error: unknown) {
handleNonCriticalError(error, {
action: 'Find matching entities',
metadata: { itemType },
});
return [];
}
}
/**
* Calculate string similarity (simple implementation)
*/
function calculateSimilarity(str1: string, str2: string): number {
const s1 = str1.toLowerCase();
const s2 = str2.toLowerCase();
if (s1 === s2) return 1.0;
if (s1.includes(s2) || s2.includes(s1)) return 0.8;
// Levenshtein distance approximation
const maxLen = Math.max(s1.length, s2.length);
const distance = levenshteinDistance(s1, s2);
return 1 - (distance / maxLen);
}
function levenshteinDistance(str1: string, str2: string): number {
const matrix: number[][] = [];
for (let i = 0; i <= str2.length; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= str1.length; j++) {
matrix[0][j] = j;
}
for (let i = 1; i <= str2.length; i++) {
for (let j = 1; j <= str1.length; j++) {
if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1];
} else {
matrix[i][j] = Math.min(
matrix[i - 1][j - 1] + 1,
matrix[i][j - 1] + 1,
matrix[i - 1][j] + 1
);
}
}
}
return matrix[str2.length][str1.length];
}
function getTableNameForItemType(itemType: string): string | null {
const typeMap: Record<string, string> = {
park: 'parks',
ride: 'rides',
manufacturer: 'companies',
operator: 'companies',
designer: 'companies',
property_owner: 'companies',
ride_model: 'ride_models',
};
return typeMap[itemType] || null;
}