![]() Server : Apache/2 System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64 User : gositeme ( 1004) PHP Version : 8.2.29 Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname Directory : /home/gositeme/domains/lavocat.ca/public_html/src/lib/ |
import { prisma } from './prisma';
interface CaseProfile {
id: string;
complexity: number;
keywords: string[];
facility: string;
caseType: string;
urgency: 'low' | 'medium' | 'high' | 'critical';
documentCount: number;
language: string;
}
interface SimilarCase {
id: string;
similarity: number;
outcome: string;
duration: number;
assignedTeam: string[];
successFactors: string[];
}
interface LawyerRecommendation {
userId: string;
name: string;
matchScore: number;
expertise: string[];
pastPerformance: {
similarCases: number;
successRate: number;
averageDuration: number;
};
availability: {
currentLoad: number;
nextAvailable: Date;
};
reasoning: string[];
}
export class SmartMatching {
/**
* Find similar cases for learning and strategy
*/
static async findSimilarCases(registrationId: string): Promise<SimilarCase[]> {
const targetCase = await prisma.registration.findUnique({
where: { id: registrationId },
include: {
documents: true,
detaineeInfo: true,
caseAssignments: {
include: { user: true }
}
}
});
if (!targetCase) return [];
const targetProfile = this.createCaseProfile(targetCase);
// Get all completed cases for comparison
const completedCases = await prisma.registration.findMany({
where: {
status: { in: ['APPROVED', 'REJECTED'] },
id: { not: registrationId }
},
include: {
documents: true,
detaineeInfo: true,
caseAssignments: {
include: { user: true }
}
}
});
const similarities: SimilarCase[] = [];
for (const case_ of completedCases) {
const caseProfile = this.createCaseProfile(case_);
const similarity = this.calculateSimilarity(targetProfile, caseProfile);
if (similarity > 0.3) { // Only include cases with >30% similarity
similarities.push({
id: case_.id,
similarity,
outcome: case_.status,
duration: this.calculateCaseDuration(case_),
assignedTeam: case_.caseAssignments.map(a => a.user.name).filter((name): name is string => name !== null),
successFactors: this.extractSuccessFactors(case_)
});
}
}
return similarities
.sort((a, b) => b.similarity - a.similarity)
.slice(0, 10); // Top 10 most similar cases
}
/**
* Get smart lawyer recommendations for a case
*/
static async getSmartRecommendations(registrationId: string): Promise<LawyerRecommendation[]> {
const targetCase = await prisma.registration.findUnique({
where: { id: registrationId },
include: { documents: true, detaineeInfo: true }
});
if (!targetCase) return [];
const targetProfile = this.createCaseProfile(targetCase);
const similarCases = await this.findSimilarCases(registrationId);
// Get all available lawyers
const lawyers = await prisma.user.findMany({
where: { role: { in: ['LAWYER', 'ADMIN', 'SUPERADMIN', 'SUPERADMIN'] } },
include: {
caseAssignments: {
include: {
registration: true
}
}
}
});
const recommendations: LawyerRecommendation[] = [];
for (const lawyer of lawyers) {
const recommendation = await this.evaluateLawyerMatch(
lawyer,
targetProfile,
similarCases
);
recommendations.push(recommendation);
}
return recommendations
.sort((a, b) => b.matchScore - a.matchScore)
.slice(0, 5); // Top 5 recommendations
}
/**
* Create a profile for case matching
*/
private static createCaseProfile(case_: any): CaseProfile {
const keywords = this.extractKeywords(case_.message + ' ' + (case_.reasonForJoining || ''));
const complexity = this.calculateComplexity(case_);
return {
id: case_.id,
complexity,
keywords,
facility: case_.detaineeInfo?.facility || 'unknown',
caseType: this.determineCaseType(case_),
urgency: this.determineUrgency(case_),
documentCount: case_.documents?.length || 0,
language: case_.preferredLanguage || 'French'
};
}
/**
* Extract keywords from case text
*/
private static extractKeywords(text: string): string[] {
const stopWords = ['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by'];
const words = text.toLowerCase()
.replace(/[^\w\s]/g, '')
.split(/\s+/)
.filter(word => word.length > 3 && !stopWords.includes(word));
// Count word frequency
const wordCount: { [key: string]: number } = {};
words.forEach(word => {
wordCount[word] = (wordCount[word] || 0) + 1;
});
// Return top keywords
return Object.entries(wordCount)
.sort(([,a], [,b]) => b - a)
.slice(0, 10)
.map(([word]) => word);
}
/**
* Calculate case complexity score
*/
private static calculateComplexity(case_: any): number {
let score = 0;
// Document count factor
score += Math.min((case_.documents?.length || 0) * 5, 25);
// Urgency factor
const urgentKeywords = ['urgent', 'court', 'hearing', 'deadline', 'appeal'];
const hasUrgency = urgentKeywords.some(keyword =>
case_.message?.toLowerCase().includes(keyword) ||
case_.urgentNeeds?.toLowerCase().includes(keyword)
);
if (hasUrgency) score += 20;
// Case type factor
const complexTypes = ['appeal', 'wrongful conviction', 'class action', 'federal'];
const isComplex = complexTypes.some(type =>
case_.message?.toLowerCase().includes(type)
);
if (isComplex) score += 30;
// Facility factor
const highSecurityFacilities = ['bordeaux', 'leclerc'];
if (highSecurityFacilities.includes(case_.detaineeInfo?.facility?.toLowerCase())) {
score += 15;
}
return Math.min(score, 100);
}
/**
* Determine case type from content
*/
private static determineCaseType(case_: any): string {
const text = (case_.message + ' ' + (case_.reasonForJoining || '')).toLowerCase();
if (text.includes('appeal')) return 'appeal';
if (text.includes('wrongful conviction')) return 'wrongful_conviction';
if (text.includes('bail')) return 'bail';
if (text.includes('sentence')) return 'sentencing';
if (text.includes('parole')) return 'parole';
if (text.includes('conditions')) return 'conditions';
return 'general';
}
/**
* Determine case urgency
*/
private static determineUrgency(case_: any): 'low' | 'medium' | 'high' | 'critical' {
const text = (case_.message + ' ' + (case_.urgentNeeds || '')).toLowerCase();
if (text.includes('urgent') || text.includes('immediate')) return 'critical';
if (text.includes('court date') || text.includes('hearing')) return 'high';
if (text.includes('deadline')) return 'medium';
return 'low';
}
/**
* Calculate similarity between two case profiles
*/
private static calculateSimilarity(profile1: CaseProfile, profile2: CaseProfile): number {
let similarity = 0;
let factors = 0;
// Keyword similarity (30% weight)
const keywordSimilarity = this.calculateKeywordSimilarity(profile1.keywords, profile2.keywords);
similarity += keywordSimilarity * 0.3;
factors += 0.3;
// Facility match (15% weight)
if (profile1.facility === profile2.facility) {
similarity += 0.15;
}
factors += 0.15;
// Case type match (20% weight)
if (profile1.caseType === profile2.caseType) {
similarity += 0.2;
}
factors += 0.2;
// Complexity similarity (20% weight)
const complexityDiff = Math.abs(profile1.complexity - profile2.complexity);
const complexitySimilarity = Math.max(0, 1 - (complexityDiff / 100));
similarity += complexitySimilarity * 0.2;
factors += 0.2;
// Urgency match (10% weight)
const urgencyLevels = ['low', 'medium', 'high', 'critical'];
const urgency1Index = urgencyLevels.indexOf(profile1.urgency);
const urgency2Index = urgencyLevels.indexOf(profile2.urgency);
const urgencyDiff = Math.abs(urgency1Index - urgency2Index);
const urgencySimilarity = Math.max(0, 1 - (urgencyDiff / 3));
similarity += urgencySimilarity * 0.1;
factors += 0.1;
// Language match (5% weight)
if (profile1.language === profile2.language) {
similarity += 0.05;
}
factors += 0.05;
return similarity / factors;
}
/**
* Calculate keyword similarity using Jaccard index
*/
private static calculateKeywordSimilarity(keywords1: string[], keywords2: string[]): number {
const set1 = new Set(keywords1);
const set2 = new Set(keywords2);
const intersection = new Set(Array.from(set1).filter(x => set2.has(x)));
const union = new Set([...keywords1, ...keywords2]);
return union.size === 0 ? 0 : intersection.size / union.size;
}
/**
* Calculate case duration in days
*/
private static calculateCaseDuration(case_: any): number {
if (!case_.createdAt || !case_.updatedAt) return 0;
const start = new Date(case_.createdAt);
const end = new Date(case_.updatedAt);
return Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24));
}
/**
* Extract success factors from completed case
*/
private static extractSuccessFactors(case_: any): string[] {
const factors: string[] = [];
if (case_.status === 'APPROVED') {
// Team composition
const teamRoles = case_.caseAssignments.map((a: any) => a.role);
if (teamRoles.includes('primary_lawyer') && teamRoles.includes('assistant_lawyer')) {
factors.push('Full legal team assigned');
}
// Quick response
const duration = this.calculateCaseDuration(case_);
if (duration < 30) {
factors.push('Resolved quickly (< 30 days)');
}
// Document preparation
if (case_.documents?.length > 3) {
factors.push('Comprehensive documentation');
}
// Specialization match
const hasSpecialistLawyer = case_.caseAssignments.some((a: any) =>
a.user.specialization &&
case_.message?.toLowerCase().includes(a.user.specialization.toLowerCase())
);
if (hasSpecialistLawyer) {
factors.push('Specialist lawyer assigned');
}
}
return factors;
}
/**
* Evaluate how well a lawyer matches a case
*/
private static async evaluateLawyerMatch(
lawyer: any,
targetProfile: CaseProfile,
similarCases: SimilarCase[]
): Promise<LawyerRecommendation> {
let matchScore = 50; // Base score
const reasoning: string[] = [];
// Experience with similar cases
const similarCaseExperience = lawyer.caseAssignments.filter((assignment: any) => {
const caseProfile = this.createCaseProfile(assignment.registration);
return this.calculateSimilarity(targetProfile, caseProfile) > 0.5;
}).length;
if (similarCaseExperience > 0) {
matchScore += Math.min(similarCaseExperience * 10, 30);
reasoning.push(`Has handled ${similarCaseExperience} similar cases`);
}
// Specialization match
if (lawyer.specialization) {
const specializationMatch = targetProfile.keywords.some(keyword =>
lawyer.specialization.toLowerCase().includes(keyword) ||
keyword.includes(lawyer.specialization.toLowerCase())
);
if (specializationMatch) {
matchScore += 20;
reasoning.push(`Specialization matches case requirements`);
}
}
// Success rate calculation
const completedCases = lawyer.caseAssignments.filter((a: any) =>
['APPROVED', 'REJECTED'].includes(a.registration.status)
);
const successfulCases = completedCases.filter((a: any) =>
a.registration.status === 'APPROVED'
);
const successRate = completedCases.length > 0 ?
(successfulCases.length / completedCases.length) * 100 : 50;
if (successRate > 80) {
matchScore += 15;
reasoning.push(`High success rate: ${successRate.toFixed(1)}%`);
} else if (successRate > 60) {
matchScore += 8;
reasoning.push(`Good success rate: ${successRate.toFixed(1)}%`);
}
// Current workload
const activeCases = lawyer.caseAssignments.filter((a: any) => a.isActive).length;
if (activeCases <= 3) {
matchScore += 10;
reasoning.push(`Optimal workload for attention to detail`);
} else if (activeCases > 6) {
matchScore -= 15;
reasoning.push(`Heavy workload may impact case attention`);
}
// Language compatibility
if (targetProfile.language !== 'French' && lawyer.name) {
// Simple heuristic - could be enhanced with actual language skills data
const englishNames = ['john', 'mary', 'david', 'sarah', 'michael'];
const isEnglishSpeaker = englishNames.some(name =>
lawyer.name.toLowerCase().includes(name)
);
if (isEnglishSpeaker) {
matchScore += 5;
reasoning.push(`Language compatibility`);
}
}
// Performance in similar cases
const avgDuration = this.calculateAverageDuration(lawyer.caseAssignments);
return {
userId: lawyer.id,
name: lawyer.name,
matchScore: Math.min(matchScore, 100),
expertise: lawyer.specialization ? [lawyer.specialization] : [],
pastPerformance: {
similarCases: similarCaseExperience,
successRate,
averageDuration: avgDuration
},
availability: {
currentLoad: activeCases,
nextAvailable: new Date() // Could be calculated based on current caseload
},
reasoning
};
}
/**
* Calculate average case duration for a lawyer
*/
private static calculateAverageDuration(assignments: any[]): number {
const completedCases = assignments.filter(a =>
['APPROVED', 'REJECTED'].includes(a.registration.status)
);
if (completedCases.length === 0) return 45; // Default estimate
const totalDuration = completedCases.reduce((sum, assignment) => {
return sum + this.calculateCaseDuration(assignment.registration);
}, 0);
return Math.round(totalDuration / completedCases.length);
}
/**
* Get case insights and recommendations
*/
static async getCaseInsights(registrationId: string): Promise<any> {
const similarCases = await this.findSimilarCases(registrationId);
const recommendations = await this.getSmartRecommendations(registrationId);
// Generate insights
const insights = {
similarCases: similarCases.slice(0, 5),
recommendedLawyers: recommendations.slice(0, 3),
predictedOutcome: this.predictOutcome(similarCases),
estimatedDuration: this.estimateDuration(similarCases),
riskFactors: this.identifyRiskFactors(similarCases),
successStrategies: this.extractSuccessStrategies(similarCases)
};
return insights;
}
/**
* Predict case outcome based on similar cases
*/
private static predictOutcome(similarCases: SimilarCase[]): any {
if (similarCases.length === 0) {
return { prediction: 'Unknown', confidence: 0 };
}
const outcomes = similarCases.map(c => c.outcome);
const approvedCount = outcomes.filter(o => o === 'APPROVED').length;
const rejectedCount = outcomes.filter(o => o === 'REJECTED').length;
const approvalRate = approvedCount / outcomes.length;
return {
prediction: approvalRate > 0.5 ? 'APPROVED' : 'REJECTED',
confidence: Math.max(approvalRate, 1 - approvalRate),
approvalRate: approvalRate * 100
};
}
/**
* Estimate case duration
*/
private static estimateDuration(similarCases: SimilarCase[]): number {
if (similarCases.length === 0) return 45; // Default
const durations = similarCases.map(c => c.duration);
return Math.round(durations.reduce((sum, d) => sum + d, 0) / durations.length);
}
/**
* Identify risk factors
*/
private static identifyRiskFactors(similarCases: SimilarCase[]): string[] {
const rejectedCases = similarCases.filter(c => c.outcome === 'REJECTED');
const riskFactors: string[] = [];
if (rejectedCases.length > similarCases.length * 0.3) {
riskFactors.push('High rejection rate for similar cases');
}
const longCases = similarCases.filter(c => c.duration > 90);
if (longCases.length > similarCases.length * 0.4) {
riskFactors.push('Similar cases tend to take longer than average');
}
return riskFactors;
}
/**
* Extract success strategies
*/
private static extractSuccessStrategies(similarCases: SimilarCase[]): string[] {
const successfulCases = similarCases.filter(c => c.outcome === 'APPROVED');
const strategies: string[] = [];
// Analyze common success factors
const allFactors = successfulCases.flatMap(c => c.successFactors);
const factorCounts: { [key: string]: number } = {};
allFactors.forEach(factor => {
factorCounts[factor] = (factorCounts[factor] || 0) + 1;
});
// Get most common success factors
Object.entries(factorCounts)
.sort(([,a], [,b]) => b - a)
.slice(0, 5)
.forEach(([factor, count]) => {
if (count > successfulCases.length * 0.3) {
strategies.push(factor);
}
});
return strategies;
}
}