1 line
143 KiB
JSON
1 line
143 KiB
JSON
{"repo": "abhiram-120/ec2-code", "files": {"src/controller/mobile/questionBank.controller.js": {"72b4870932a1d237646b6cf563d8a227a824d536": {"t": 1785745556.7900355, "n": 70956, "h": "0bc81533e5941168", "c": "// Complete questionBank.controller.js with FIXED teacher distribution\n\nconst bcrypt = require('bcrypt');\nconst Joi = require('joi');\nconst axios = require('axios');\n\nconst Users = require('../../models/users');\nconst QuestionBank = require('../../models/questionBank');\nconst UserQuestionResponse = require('../../models/userQuestionResponse');\nconst UserTeacherRecommendation = require('../../models/userTeacherRecommendation');\nconst e = require('cors');\nconst { DataTypes, Op } = require('sequelize');\nconst UserOccupation = require('../../models/usersOccupation');\nconst UserReview = require('../../models/userReviews');\n\n// AI API Configuration\nconst DEEPSEEK_API_KEY = 'sk-0500f22e4577455f959763dabba7ae43';\nconst OPENAI_API_KEY = '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';\n\nconst DEEPSEEK_API_URL = 'https://api.deepseek.com/chat/completions';\nconst OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions';\n\n// Get all active questions ordered by question_order\nasync function getAllActiveQuestions(req, res) {\n try {\n // Get language from query parameter, default to 'en'\n const language = req.query.language || 'en';\n \n // Fetch all active questions ordered by question_order then created_at\n const questions = await QuestionBank.findAll({\n where: { \n is_active: 1 \n },\n attributes: ['id', 'question', 'type', 'options', 'question_order', 'created_at'],\n order: [['question_order', 'ASC'], ['created_at', 'ASC']]\n });\n\n if (!questions || questions.length === 0) {\n return res.status(404).json({ \n status: 'error', \n message: 'No active questions found' \n });\n }\n\n // Format questions for mobile response with localization\n const formattedQuestions = questions.map(question => {\n return {\n id: question.id,\n question: question.getLocalizedQuestion(language),\n type: question.type,\n options: question.getLocalizedOptions(language),\n order: question.question_order,\n created_at: question.created_at\n };\n });\n\n return res.status(200).json({ \n status: 'success', \n message: 'Questions retrieved successfully',\n count: formattedQuestions.length,\n data: formattedQuestions \n });\n\n } catch (err) {\n console.error('Error fetching questions:', err);\n return res.status(500).json({\n status: 'error',\n message: 'Internal server error while fetching questions'\n });\n }\n}\n\n// Get questions by type ordered by question_order\nasync function getQuestionsByType(req, res) {\n try {\n const { type } = req.params;\n const language = req.query.language || 'en';\n \n // Validate question type\n const validTypes = ['single-choice', 'multiple-choice', 'checkbox', 'yes-no', 'text'];\n if (!validTypes.includes(type)) {\n return res.status(400).json({\n status: 'error',\n message: `Invalid question type. Valid types are: ${validTypes.join(', ')}`\n });\n }\n\n // Fetch questions by type ordered by question_order then created_at\n const questions = await QuestionBank.findAll({\n where: { \n is_active: 1,\n type: type\n },\n attributes: ['id', 'question', 'type', 'options', 'question_order', 'created_at'],\n order: [['question_order', 'ASC'], ['created_at', 'ASC']]\n });\n\n if (!questions || questions.length === 0) {\n return res.status(404).json({ \n status: 'error', \n message: `No active questions found for type: ${type}` \n });\n }\n\n // Format questions for mobile response with localization\n const formattedQuestions = questions.map(question => {\n return {\n id: question.id,\n question: question.getLocalizedQuestion(language),\n type: question.type,\n options: question.getLocalizedOptions(language),\n order: question.question_order,\n created_at: question.created_at\n };\n });\n\n return res.status(200).json({ \n status: 'success', \n message: `Questions of type '${type}' retrieved successfully`,\n count: formattedQuestions.length,\n data: formattedQuestions \n });\n\n } catch (err) {\n console.error('Error fetching questions by type:', err);\n return res.status(500).json({\n status: 'error',\n message: 'Internal server error while fetching questions'\n });\n }\n}\n\n// Helper function to validate if selected options exist in question options\nfunction validateSelectedOptions(selectedOptions, questionOptions, questionType) {\n if (questionType === 'text') {\n return { isValid: true };\n }\n \n if (!selectedOptions || !Array.isArray(selectedOptions)) {\n return { isValid: false, message: 'Selected options must be an array' };\n }\n \n if (!questionOptions || !Array.isArray(questionOptions)) {\n return { isValid: false, message: 'Question options not found' };\n }\n \n // Check if all selected options exist in question options\n for (const selectedOption of selectedOptions) {\n if (!questionOptions.includes(selectedOption)) {\n return { \n isValid: false, \n message: `Selected option \"${selectedOption}\" is not valid for this question` \n };\n }\n }\n \n return { isValid: true };\n}\n\n// Store user response to a question - UPDATED TO ACCEPT ACTUAL VALUES\nasync function storeQuestionResponse(req, res) {\n try {\n const { questionId, responseText, selectedOptions } = req.body;\n const userId = req.userId; // From auth middleware\n const language = req.query.language || 'en';\n \n // Validate required fields\n if (!questionId) {\n return res.status(400).json({\n status: 'error',\n message: 'Question ID is required'\n });\n }\n \n // Check if question exists and is active\n const question = await QuestionBank.findOne({\n where: {\n id: questionId,\n is_active: 1\n }\n });\n \n if (!question) {\n return res.status(404).json({\n status: 'error',\n message: 'Question not found or inactive'\n });\n }\n \n // Validate response based on question type\n const validationResult = validateResponse(question.type, responseText, selectedOptions);\n if (!validationResult.isValid) {\n return res.status(400).json({\n status: 'error',\n message: validationResult.message\n });\n }\n \n // For choice questions, validate that selected options exist in question options\n if (question.type !== 'text' && selectedOptions && Array.isArray(selectedOptions)) {\n const questionOptions = question.getLocalizedOptions(language);\n const optionValidation = validateSelectedOptions(selectedOptions, questionOptions, question.type);\n if (!optionValidation.isValid) {\n return res.status(400).json({\n status: 'error',\n message: optionValidation.message\n });\n }\n }\n \n // Check if user already responded to this question\n const existingResponse = await UserQuestionResponse.findOne({\n where: {\n user_id: userId,\n question_id: questionId\n }\n });\n \n let userResponse;\n \n if (existingResponse) {\n // Update existing response\n userResponse = await existingResponse.update({\n response_text: responseText || null,\n selected_options: selectedOptions || null, // Store actual values directly\n question_type: question.type,\n updated_at: new Date()\n });\n } else {\n // Create new response\n userResponse = await UserQuestionResponse.create({\n user_id: userId,\n question_id: questionId,\n response_text: responseText || null,\n selected_options: selectedOptions || null, // Store actual values directly\n question_type: question.type\n });\n }\n \n // Format response for mobile\n const formattedResponse = {\n id: userResponse.id,\n questionId: userResponse.question_id,\n responseText: userResponse.response_text,\n selectedOptions: userResponse.selected_options,\n questionType: userResponse.question_type,\n createdAt: userResponse.created_at,\n updatedAt: userResponse.updated_at\n };\n \n return res.status(200).json({\n status: 'success',\n message: existingResponse ? 'Response updated successfully' : 'Response stored successfully',\n data: formattedResponse\n });\n \n } catch (err) {\n console.error('Error storing question response:', err);\n return res.status(500).json({\n status: 'error',\n message: 'Internal server error while storing response'\n });\n }\n}\n\n// 🆕 NEW FUNCTION: Check user's questionnaire completion status\nasync function getUserQuestionnaireStatus(req, res) {\n try {\n const userId = req.userId; // From auth middleware\n const language = req.query.language || 'en';\n \n // Get all active questions count\n const totalActiveQuestions = await QuestionBank.count({\n where: { \n is_active: 1 \n }\n });\n \n if (totalActiveQuestions === 0) {\n return res.status(200).json({\n success: true,\n message: 'No active questions available',\n data: {\n status: 'no_questions',\n shouldShowQuestionnaire: false,\n hasCompletedQuestionnaire: false,\n hasCachedRecommendations: false,\n completionPercentage: 0,\n totalQuestions: 0,\n answeredQuestions: 0,\n action: 'no_action',\n lastAnsweredAt: null\n }\n });\n }\n \n // Get user's responses (only for active questions)\n const userResponses = await UserQuestionResponse.findAll({\n where: {\n user_id: userId\n },\n attributes: ['question_id', 'created_at'],\n include: [\n {\n model: QuestionBank,\n as: 'question', // This requires the association to be set up\n where: { is_active: 1 },\n required: true,\n attributes: ['id']\n }\n ]\n });\n \n const answeredQuestions = userResponses.length;\n const completionPercentage = Math.round((answeredQuestions / totalActiveQuestions) * 100);\n \n // Check if user has cached recommendations\n let hasCachedRecommendations = false;\n try {\n const cachedRecommendation = await UserTeacherRecommendation.findOne({\n where: {\n user_id: userId,\n is_active: true,\n deleted_at: null\n },\n order: [['created_at', 'DESC']]\n });\n \n hasCachedRecommendations = cachedRecommendation && cachedRecommendation.isValid();\n } catch (cacheError) {\n console.log('Cache check failed:', cacheError);\n hasCachedRecommendations = false;\n }\n \n // Determine user status and required action\n let status, shouldShowQuestionnaire, hasCompletedQuestionnaire, action, message;\n \n if (answeredQuestions === 0) {\n // User hasn't answered any questions\n status = 'not_started';\n shouldShowQuestionnaire = true;\n hasCompletedQuestionnaire = false;\n action = 'show_questionnaire';\n message = 'User needs to complete the questionnaire';\n \n } else if (answeredQuestions < totalActiveQuestions) {\n // 🎯 YOUR USE CASE: User has partially completed questionnaire\n status = 'partially_completed';\n shouldShowQuestionnaire = true;\n hasCompletedQuestionnaire = false;\n action = 'continue_questionnaire';\n message = `User has answered ${answeredQuestions} out of ${totalActiveQuestions} questions`;\n \n } else if (answeredQuestions === totalActiveQuestions) {\n // User has completed all questions\n hasCompletedQuestionnaire = true;\n \n if (hasCachedRecommendations) {\n // User has completed questionnaire and has cached recommendations\n status = 'completed_with_cache';\n shouldShowQuestionnaire = false;\n action = 'show_recommendations';\n message = 'User has completed questionnaire and has cached recommendations';\n } else {\n // User completed questionnaire but no cached recommendations\n status = 'completed_no_cache';\n shouldShowQuestionnaire = false;\n action = 'generate_recommendations';\n message = 'User completed questionnaire but needs to generate recommendations';\n }\n } else {\n // Edge case: user has more responses than active questions (old responses)\n status = 'over_completed';\n shouldShowQuestionnaire = false;\n hasCompletedQuestionnaire = true;\n action = hasCachedRecommendations ? 'show_recommendations' : 'generate_recommendations';\n message = 'User has responses but may need to update questionnaire';\n }\n \n return res.status(200).json({\n success: true,\n message: message,\n data: {\n status: status, // 'not_started', 'partially_completed', 'completed_with_cache', 'completed_no_cache', 'over_completed'\n shouldShowQuestionnaire: shouldShowQuestionnaire, // 🎯 YOUR MAIN BOOLEAN CHECK\n hasCompletedQuestionnaire: hasCompletedQuestionnaire,\n hasCachedRecommendations: hasCachedRecommendations,\n completionPercentage: completionPercentage, // 0-100: percentage of questions completed\n totalQuestions: totalActiveQuestions,\n answeredQuestions: answeredQuestions,\n action: action, // 'show_questionnaire', 'continue_questionnaire', 'show_recommendations', 'generate_recommendations'\n lastAnsweredAt: userResponses.length > 0 ? \n Math.max(...userResponses.map(r => new Date(r.created_at).getTime())) : null\n }\n });\n \n } catch (err) {\n console.error('Error checking user questionnaire status:', err);\n return res.status(500).json({\n success: false,\n message: 'Internal server error while checking questionnaire status',\n data: {\n status: 'error',\n shouldShowQuestionnaire: true, // Default to showing questionnaire on error\n hasCompletedQuestionnaire: false,\n hasCachedRecommendations: false,\n completionPercentage: 0,\n totalQuestions: 0,\n answeredQuestions: 0,\n action: 'show_questionnaire',\n lastAnsweredAt: null\n }\n });\n }\n}\n\n// SMART TEACHER PRE-FILTERING FUNCTIONS\nfunction extractStudentPreferences(studentResponses) {\n const preferences = {\n languages: [],\n subjects: [],\n level: 'intermediate',\n goals: [],\n preferredTeachingStyle: [],\n schedulePreferences: []\n };\n \n studentResponses.forEach(response => {\n const questionText = response.questionText.toLowerCase();\n \n // Extract language preferences\n if (questionText.includes('language') || questionText.includes('speak')) {\n if (response.selectedOptions && response.selectedOptions.length > 0) {\n preferences.languages.push(...response.selectedOptions);\n }\n }\n \n // Extract subject interests\n if (questionText.includes('subject') || questionText.includes('topic') || questionText.includes('focus')) {\n if (response.selectedOptions && response.selectedOptions.length > 0) {\n preferences.subjects.push(...response.selectedOptions);\n }\n }\n \n // Extract proficiency level - FIXED: Now expects actual text values\n if (questionText.includes('level') || questionText.includes('proficiency')) {\n if (response.selectedOptions && response.selectedOptions.length > 0) {\n console.log('response :', response);\n \n // Now we expect actual text values, not indices\n const levelOption = String(response.selectedOptions[0]).toLowerCase();\n \n if (levelOption.includes('beginner') || levelOption.includes('a1') || levelOption.includes('a2')) {\n preferences.level = 'beginner';\n } else if (levelOption.includes('advanced') || levelOption.includes('c1') || levelOption.includes('c2')) {\n preferences.level = 'advanced';\n } else {\n preferences.level = 'intermediate';\n }\n }\n }\n \n // Extract learning goals\n if (questionText.includes('goal') || questionText.includes('purpose') || questionText.includes('why')) {\n if (response.selectedOptions && response.selectedOptions.length > 0) {\n preferences.goals.push(...response.selectedOptions);\n }\n }\n });\n \n return preferences;\n}\n\nasync function convertIndicesToOptionValues(responses, language = 'en') {\n try {\n const questionIds = [...new Set(responses.map(r => r.questionId))];\n \n // Fetch question data including options\n const questions = await QuestionBank.findAll({\n where: {\n id: { [Op.in]: questionIds },\n is_active: 1\n },\n attributes: ['id', 'question', 'type', 'options', 'question_order']\n });\n \n const questionMap = {};\n questions.forEach(q => {\n questionMap[q.id] = {\n text: q.getLocalizedQuestion(language),\n type: q.type,\n options: q.getLocalizedOptions(language), // Get actual option text array\n order: q.question_order\n };\n });\n \n // Convert responses with indices to actual option values\n const convertedResponses = responses.map(response => {\n const questionInfo = questionMap[response.questionId];\n \n if (!questionInfo) {\n console.log(`⚠️ Question ${response.questionId} not found`);\n return {\n questionId: response.questionId,\n questionText: `Question ${response.questionId}`,\n questionType: response.responseText ? 'text' : 'choice',\n responseText: response.responseText,\n selectedOptions: response.selectedOptions,\n order: response.questionId\n };\n }\n \n let convertedSelectedOptions = null;\n \n // Convert indices to actual option text if needed\n if (response.selectedOptions && Array.isArray(response.selectedOptions) && questionInfo.options) {\n convertedSelectedOptions = response.selectedOptions.map(optionValue => {\n // Check if it's an index (number) that needs conversion\n if (typeof optionValue === 'number' && questionInfo.options[optionValue] !== undefined) {\n const actualValue = questionInfo.options[optionValue];\n console.log(`🔧 Question ${response.questionId}: Converted index ${optionValue} to \"${actualValue}\"`);\n return actualValue;\n }\n // If it's already a string or invalid index, return as-is\n return String(optionValue);\n });\n } else {\n convertedSelectedOptions = response.selectedOptions;\n }\n \n return {\n questionId: response.questionId,\n questionText: questionInfo.text,\n questionType: questionInfo.type,\n responseText: response.responseText,\n selectedOptions: convertedSelectedOptions,\n order: questionInfo.order\n };\n }).sort((a, b) => a.order - b.order);\n \n return convertedResponses;\n \n } catch (error) {\n console.error('❌ Error converting indices to option values:', error);\n \n // Fallback: return responses as-is with basic formatting\n return responses.map(response => ({\n questionId: response.questionId,\n questionText: `Question ${response.questionId}`,\n questionType: response.responseText ? 'text' : 'choice',\n responseText: response.responseText,\n selectedOptions: response.selectedOptions,\n order: response.questionId\n }));\n }\n}\n\nfunction calculateTeacherScore(teacher, studentPreferences) {\n let score = 0;\n let maxScore = 0;\n \n // 1. Language compatibility (25 points)\n maxScore += 25;\n if (teacher.speakingLanguages && teacher.speakingLanguages.length > 0) {\n const languageMatch = studentPreferences.languages.some(lang => \n teacher.speakingLanguages.some(teacherLang => \n teacherLang.toLowerCase().includes(lang.toLowerCase()) ||\n lang.toLowerCase().includes(teacherLang.toLowerCase())\n )\n );\n if (languageMatch) score += 25;\n else if (teacher.speakingLanguages.includes('English')) score += 15; // English as fallback\n }\n \n // 2. Subject/Specialty match (20 points)\n maxScore += 20;\n if (teacher.specialties && teacher.specialties.length > 0) {\n const subjectMatch = studentPreferences.subjects.some(subject =>\n teacher.specialties.some(specialty =>\n specialty.toLowerCase().includes(subject.toLowerCase()) ||\n subject.toLowerCase().includes(specialty.toLowerCase())\n )\n );\n if (subjectMatch) score += 20;\n \n // Also check teachings\n if (teacher.teachings && teacher.teachings.length > 0) {\n const teachingMatch = studentPreferences.subjects.some(subject =>\n teacher.teachings.some(teaching =>\n teaching.toLowerCase().includes(subject.toLowerCase()) ||\n subject.toLowerCase().includes(teaching.toLowerCase())\n )\n );\n if (teachingMatch) score += 10; // Bonus for teaching match\n }\n }\n \n // 3. Teaching level appropriateness (15 points)\n maxScore += 15;\n if (teacher.levels && teacher.levels.length > 0) {\n const levelMap = {\n 'beginner': ['a1', 'a2', 'beginner', 'basic'],\n 'intermediate': ['b1', 'b2', 'intermediate', 'general'],\n 'advanced': ['c1', 'c2', 'advanced', 'business', 'academic']\n };\n \n const appropriateLevels = levelMap[studentPreferences.level] || [];\n const levelMatch = teacher.levels.some(level =>\n appropriateLevels.some(appLevel =>\n level.toLowerCase().includes(appLevel)\n )\n );\n if (levelMatch) score += 15;\n }\n \n // 4. Review quality (20 points)\n maxScore += 20;\n if (teacher.reviews && teacher.reviews.totalReviews > 0) {\n const rating = parseFloat(teacher.reviews.averageRating);\n if (rating >= 4.5) score += 20;\n else if (rating >= 4.0) score += 15;\n else if (rating >= 3.5) score += 10;\n else if (rating >= 3.0) score += 5;\n \n // Bonus for having multiple reviews\n if (teacher.reviews.totalReviews >= 10) score += 5;\n else if (teacher.reviews.totalReviews >= 5) score += 3;\n }\n \n // 5. Profile completeness (10 points)\n maxScore += 10;\n let completenessScore = 0;\n if (teacher.headline && teacher.headline.length > 10) completenessScore += 2;\n if (teacher.bio && teacher.bio.length > 50) completenessScore += 2;\n if (teacher.videoDemo) completenessScore += 3;\n if (teacher.education && teacher.education.length > 5) completenessScore += 1;\n if (teacher.experience && teacher.experience.length > 5) completenessScore += 1;\n if (teacher.about && teacher.about.length > 20) completenessScore += 1;\n score += completenessScore;\n \n // 6. Teaching variety (10 points)\n maxScore += 10;\n const totalTeachingAreas = (teacher.teachings ? teacher.teachings.length : 0) +\n (teacher.specialties ? teacher.specialties.length : 0);\n if (totalTeachingAreas >= 5) score += 10;\n else if (totalTeachingAreas >= 3) score += 7;\n else if (totalTeachingAreas >= 1) score += 4;\n \n // Return percentage score\n return maxScore > 0 ? Math.round((score / maxScore) * 100) : 0;\n}\n\nfunction filterBestTeachers(allTeachers, studentResponses, limit = 6) {\n console.log(`🎯 Starting smart filtering from ${allTeachers.length} teachers...`);\n \n // Extract student preferences\n const studentPreferences = extractStudentPreferences(studentResponses);\n console.log('📊 Student preferences:', studentPreferences);\n \n // Score all teachers\n const scoredTeachers = allTeachers.map(teacher => {\n const score = calculateTeacherScore(teacher, studentPreferences);\n return {\n ...teacher,\n compatibilityScore: score\n };\n });\n \n // Sort by score and take top teachers\n const sortedTeachers = scoredTeachers.sort((a, b) => {\n // Primary sort by compatibility score\n if (b.compatibilityScore !== a.compatibilityScore) {\n return b.compatibilityScore - a.compatibilityScore;\n }\n \n // Secondary sort by review rating\n const aRating = a.reviews?.averageRating || 0;\n const bRating = b.reviews?.averageRating || 0;\n if (bRating !== aRating) {\n return bRating - aRating;\n }\n \n // Tertiary sort by number of reviews\n const aReviews = a.reviews?.totalReviews || 0;\n const bReviews = b.reviews?.totalReviews || 0;\n return bReviews - aReviews;\n });\n \n const selectedTeachers = sortedTeachers.slice(0, limit);\n const remainingTeachers = sortedTeachers.slice(limit);\n \n console.log(`✅ Selected top ${selectedTeachers.length} teachers with scores:`,\n selectedTeachers.map(t => `${t.name} (${t.compatibilityScore}%)`).join(', ')\n );\n \n return {\n selectedTeachers,\n remainingTeachers,\n studentPreferences\n };\n}\n\n// AI RECOMMENDATION FUNCTIONS (Modified for 6 teachers only)\nasync function callDeepSeekAPI(prompt) {\n try {\n console.log('🤖 Calling DeepSeek API...');\n \n const response = await axios.post(DEEPSEEK_API_URL, {\n model: \"deepseek-chat\",\n messages: [\n {\n role: \"system\",\n content: \"You are an expert teacher matching system. Analyze student responses and pre-selected teacher profiles to provide final ranking. Always respond with valid JSON format.\"\n },\n {\n role: \"user\",\n content: prompt\n }\n ],\n max_tokens: 1500,\n temperature: 0.7\n }, {\n headers: {\n 'Authorization': `Bearer ${DEEPSEEK_API_KEY}`,\n 'Content-Type': 'application/json'\n },\n timeout: 30000\n });\n \n console.log('✅ DeepSeek API response received');\n return {\n success: true,\n data: response.data.choices[0].message.content,\n provider: 'DeepSeek'\n };\n \n } catch (error) {\n console.error('❌ DeepSeek API Error:', error.response?.data || error.message);\n return {\n success: false,\n error: error.response?.data?.error?.message || error.message,\n provider: 'DeepSeek'\n };\n }\n}\n\nasync function callOpenAI(prompt) {\n try {\n console.log('🤖 Calling OpenAI API (fallback)...');\n \n const response = await axios.post(OPENAI_API_URL, {\n model: \"gpt-3.5-turbo\",\n messages: [\n {\n role: \"system\",\n content: \"You are an expert teacher matching system. Analyze student responses and pre-selected teacher profiles to provide final ranking. Always respond with valid JSON format.\"\n },\n {\n role: \"user\",\n content: prompt\n }\n ],\n max_tokens: 1500,\n temperature: 0.7\n }, {\n headers: {\n 'Authorization': `Bearer ${OPENAI_API_KEY}`,\n 'Content-Type': 'application/json'\n },\n timeout: 30000\n });\n \n console.log('✅ OpenAI API response received');\n return {\n success: true,\n data: response.data.choices[0].message.content,\n provider: 'OpenAI'\n };\n \n } catch (error) {\n console.error('❌ OpenAI API Error:', error.response?.data || error.message);\n return {\n success: false,\n error: error.response?.data?.error?.message || error.message,\n provider: 'OpenAI'\n };\n }\n}\n\n// 🔧 UPDATED: getFinalTeacherRanking with fallback for missing nextThreeTeachers\nasync function getFinalTeacherRanking(studentResponses, selectedTeachers) {\n const prompt = `\nAnalyze this student's learning profile and provide final ranking for these ${selectedTeachers.length} PRE-SELECTED teachers:\n\nSTUDENT PROFILE:\n${studentResponses.map(response => {\n if (response.selectedOptions) {\n return `Question: ${response.questionText}\nAnswer: ${JSON.stringify(response.selectedOptions)}`;\n } else {\n return `Question: ${response.questionText}\nAnswer: \"${response.responseText}\"`;\n }\n}).join('\\n\\n')}\n\n${selectedTeachers.length} PRE-SELECTED TEACHERS (already filtered for basic compatibility):\n${selectedTeachers.map((teacher, index) => {\n const teachingsText = teacher.teachings && teacher.teachings.length > 0 \n ? teacher.teachings.join(', ') \n : 'General English';\n \n const languagesText = teacher.speakingLanguages && teacher.speakingLanguages.length > 0 \n ? teacher.speakingLanguages.join(', ') \n : teacher.language || 'English';\n \n const specialtiesText = teacher.specialties && teacher.specialties.length > 0 \n ? teacher.specialties.join(', ') \n : 'General Teaching';\n \n const levelsText = teacher.levels && teacher.levels.length > 0 \n ? teacher.levels.join(', ') \n : 'All levels';\n \n const reviewSummary = teacher.reviews && teacher.reviews.totalReviews > 0\n ? `${teacher.reviews.totalReviews} reviews (${teacher.reviews.averageRating}/5.0)`\n : 'No reviews yet';\n \n const recentReviewsText = teacher.reviews && teacher.reviews.recentReviews.length > 0\n ? teacher.reviews.recentReviews.map(review => \n `\"${review.description}\" (${review.overallRating}/5)`\n ).join('; ')\n : 'No detailed feedback';\n\n return `Teacher ${index + 1}:\n - ID: ${teacher.id}\n - Name: ${teacher.name}\n - Headline: ${teacher.headline || 'Experienced Teacher'}\n - Bio: ${teacher.bio || 'Dedicated to helping students achieve their goals'}\n - Experience: ${teacher.experience || 'Professional teaching experience'}\n - Compatibility Score: ${teacher.compatibilityScore}%\n \n TEACHING PROFILE:\n - Teaches: ${teachingsText}\n - Languages: ${languagesText}\n - Specializations: ${specialtiesText}\n - Levels: ${levelsText}\n \n STUDENT FEEDBACK:\n - Reviews: ${reviewSummary}\n - Recent Comments: ${recentReviewsText}\n \n EXTRAS:\n - Video Demo: ${teacher.videoDemo ? 'Available' : 'Not available'}\n - Timezone: ${teacher.timezone || 'Flexible'}`;\n }).join('\\n\\n')}\n\n RANKING INSTRUCTIONS:\n These ${selectedTeachers.length} teachers are already pre-filtered for basic compatibility. Your job is to provide nuanced final ranking based on:\n\n 1. **Deep Teaching Match**: How well does their teaching style/approach fit student's specific learning goals?\n 2. **Communication Fit**: Language comfort level and teaching personality match\n 3. **Experience Relevance**: Specific experience in areas student wants to improve\n 4. **Student Success Track**: Quality of student feedback and teaching effectiveness\n 5. **Learning Style Alignment**: Teaching methods that match student's preferred learning approach\n\n RESPONSE REQUIREMENTS:\n - You MUST rank all ${selectedTeachers.length} teachers\n - Top 3 = Best matches (topThreeTeachers) - positions 1-3\n - Next 3 = Good alternatives (nextThreeTeachers) - positions 4-6 \n - Provide detailed, specific match reasons (2-3 per teacher)\n - Include match scores (85-99 for top 3, 70-84 for next 3)\n\n Return ONLY valid JSON (no extra text) in this UPDATED format:\n {\n \"topThreeTeachers\": [\n {\n \"teacherId\": 123,\n \"name\": \"Teacher Name\",\n \"matchScore\": 95,\n \"matchReasons\": [\n \"Specializes in exactly what student needs (Business English)\",\n \"Excellent student feedback with 4.8/5 rating from 25+ students\",\n \"Teaching style matches student's preference for interactive learning\"\n ]\n }\n ],\n \"nextThreeTeachers\": [\n {\n \"teacherId\": 456,\n \"name\": \"Teacher Name\", \n \"matchScore\": 78,\n \"matchReasons\": [\n \"Good general English experience with solid 4.2/5 rating\",\n \"Available for student's preferred schedule\",\n \"Professional approach with video demo\"\n ]\n }\n ],\n \"analysis\": {\n \"studentLevel\": \"Intermediate\",\n \"primaryGoals\": [\"Business English\", \"Speaking Confidence\"],\n \"keyPreferences\": [\"Interactive lessons\", \"Native speaker\", \"Evening availability\"],\n \"rankingSummary\": \"Prioritized teachers with business English expertise and interactive teaching methods based on student's professional goals\"\n }\n }`;\n\n // Try DeepSeek first, then OpenAI as fallback\n let aiResponse = await callDeepSeekAPI(prompt);\n \n if (!aiResponse.success) {\n console.log('🔄 DeepSeek failed, trying OpenAI...');\n aiResponse = await callOpenAI(prompt);\n }\n \n if (!aiResponse.success) {\n console.error('❌ Both AI APIs failed');\n return {\n success: false,\n error: 'Both DeepSeek and OpenAI APIs failed',\n fallbackRecommendation: createSimpleFallback(selectedTeachers)\n };\n }\n \n try {\n // Clean up and parse JSON response\n let responseText = aiResponse.data.trim();\n \n console.log('📝 Raw AI Response:', responseText.substring(0, 200) + '...');\n \n // Remove markdown code block markers if present\n if (responseText.startsWith('```json')) {\n responseText = responseText.replace(/^```json\\s*/, '').replace(/\\s*```$/, '');\n }\n \n // Find the actual JSON content\n const jsonStart = responseText.indexOf('{');\n const jsonEnd = responseText.lastIndexOf('}') + 1;\n \n if (jsonStart !== -1 && jsonEnd > jsonStart) {\n responseText = responseText.substring(jsonStart, jsonEnd);\n }\n \n const parsedResponse = JSON.parse(responseText);\n \n // Validate response structure\n if (!parsedResponse.topThreeTeachers) {\n throw new Error('Invalid response structure - missing topThreeTeachers');\n }\n \n // 🆕 NEW: Handle missing or incomplete nextThreeTeachers\n if (!parsedResponse.nextThreeTeachers || parsedResponse.nextThreeTeachers.length === 0) {\n console.log('⚠️ AI did not return nextThreeTeachers, creating fallback...');\n \n // Get teacher IDs that are already in topThreeTeachers\n const topThreeIds = parsedResponse.topThreeTeachers.map(t => t.teacherId);\n \n // Find remaining teachers from the selected teachers\n const remainingFromSelected = selectedTeachers.filter(teacher => \n !topThreeIds.includes(teacher.id)\n );\n \n // Create nextThreeTeachers from remaining teachers\n parsedResponse.nextThreeTeachers = remainingFromSelected.slice(0, 3).map((teacher, index) => ({\n teacherId: teacher.id,\n name: teacher.name,\n matchScore: 75 - (index * 3), // Decreasing scores: 75, 72, 69\n matchReasons: [\n 'Good compatibility with student profile',\n teacher.reviews?.totalReviews > 0 \n ? `Rated ${teacher.reviews.averageRating}/5 by ${teacher.reviews.totalReviews} students`\n : 'Professional teaching experience',\n 'Available for lessons'\n ]\n }));\n \n console.log('✅ Created fallback nextThreeTeachers:', \n parsedResponse.nextThreeTeachers.map(t => `${t.name} (${t.matchScore}%)`).join(', ')\n );\n }\n \n console.log('✅ Successfully parsed AI ranking');\n console.log('🥇 Top teachers:', parsedResponse.topThreeTeachers?.map(t => `${t.name} (${t.matchScore}%)`).join(', '));\n console.log('🥈 Next teachers:', parsedResponse.nextThreeTeachers?.map(t => `${t.name} (${t.matchScore}%)`).join(', '));\n \n return {\n success: true,\n data: parsedResponse,\n provider: aiResponse.provider\n };\n \n } catch (parseError) {\n console.error('❌ Failed to parse AI response:', parseError);\n \n return {\n success: false,\n error: 'Failed to parse AI response',\n fallbackRecommendation: createSimpleFallback(selectedTeachers),\n provider: aiResponse.provider\n };\n }\n}\n\n// 🔧 UPDATED: createSimpleFallback to ensure proper distribution\nfunction createSimpleFallback(selectedTeachers) {\n // Simple fallback: split based on compatibility scores and reviews\n const sorted = [...selectedTeachers].sort((a, b) => {\n const aScore = (a.compatibilityScore || 0) + (a.reviews?.averageRating || 0) * 10;\n const bScore = (b.compatibilityScore || 0) + (b.reviews?.averageRating || 0) * 10;\n return bScore - aScore;\n });\n \n const top3 = sorted.slice(0, 3);\n const next3 = sorted.slice(3, 6); // 🆕 NEW: Next 3 teachers\n \n return {\n // Top 3 teachers (positions 1-3)\n topThreeTeachers: top3.map((teacher, index) => ({\n teacherId: teacher.id,\n name: teacher.name,\n matchScore: Math.max(80, teacher.compatibilityScore || 80) + (3 - index) * 3,\n matchReasons: [\n `High compatibility score (${teacher.compatibilityScore}%)`,\n teacher.reviews?.totalReviews > 0 \n ? `Rated ${teacher.reviews.averageRating}/5 by ${teacher.reviews.totalReviews} students`\n : 'Experienced professional teacher',\n teacher.videoDemo ? 'Professional video demo available' : 'Comprehensive teaching profile'\n ]\n })),\n \n // 🆕 NEW: Next 3 teachers (positions 4-6)\n nextThreeTeachers: next3.map((teacher, index) => ({\n teacherId: teacher.id,\n name: teacher.name,\n matchScore: Math.max(60, (teacher.compatibilityScore || 60) - 5) + (3 - index) * 2,\n matchReasons: [\n 'Good teaching qualifications',\n teacher.reviews?.totalReviews > 0 \n ? `${teacher.reviews.totalReviews} student reviews`\n : 'Professional teaching experience',\n 'Available for lessons'\n ]\n })),\n \n // Analysis remains the same\n analysis: {\n studentLevel: \"Not determined\",\n primaryGoals: [\"General English improvement\"],\n keyPreferences: [\"Not specified\"],\n rankingSummary: \"Fallback ranking based on teacher profiles and ratings\"\n }\n };\n}\n\n// 🔧 ENHANCED MAIN FUNCTION WITH FIXED TEACHER DISTRIBUTION\nasync function storeMultipleResponses(req, res) {\n try {\n const { responses, forceRefresh = false } = req.body;\n console.log('req :',req.userId);\n const userId = req.userId;\n const language = req.query.language || 'en';\n \n // Validate input\n if (!Array.isArray(responses) || responses.length === 0) {\n return res.status(400).json({\n success: false,\n message: 'Responses must be a non-empty array',\n data: null\n });\n }\n \n const results = [];\n const errors = [];\n \n // Process and store responses (keeping existing logic)\n for (let i = 0; i < responses.length; i++) {\n const { questionId, responseText, selectedOptions } = responses[i];\n \n try {\n if (!questionId) {\n errors.push({ questionId, error: 'Question ID is required' });\n continue;\n }\n \n const question = await QuestionBank.findOne({\n where: { id: questionId, is_active: 1 }\n });\n \n if (!question) {\n errors.push({ questionId, error: 'Question not found or inactive' });\n continue;\n }\n \n if (!responseText && (!selectedOptions || selectedOptions.length === 0)) {\n errors.push({ questionId, error: 'Response is required' });\n continue;\n }\n \n const existingResponse = await UserQuestionResponse.findOne({\n where: { user_id: userId, question_id: questionId }\n });\n \n let userResponse;\n \n if (existingResponse) {\n userResponse = await existingResponse.update({\n response_text: responseText || null,\n selected_options: selectedOptions || null,\n question_type: question.type,\n updated_at: new Date()\n });\n } else {\n userResponse = await UserQuestionResponse.create({\n user_id: userId,\n question_id: questionId,\n response_text: responseText || null,\n selected_options: selectedOptions || null,\n question_type: question.type\n });\n }\n \n results.push({ questionId: userResponse.question_id });\n \n } catch (error) {\n console.error(`Error processing response ${i}:`, error);\n errors.push({ questionId, error: 'Failed to process response' });\n }\n }\n \n // If too many errors, return early\n if (errors.length >= responses.length) {\n return res.status(400).json({\n success: false,\n message: 'Failed to process responses',\n data: null,\n errors: errors\n });\n }\n \n // 🚀 NEW: Convert indices to actual option values for analysis\n console.log('🔧 Converting option indices to actual values...');\n const studentResponseData = await convertIndicesToOptionValues(responses, language);\n console.log('forceRefresh :',forceRefresh);\n // 🚀 NEW: CHECK CACHE FIRST (unless forceRefresh is true)\n if (!forceRefresh) {\n try {\n console.log('🔍 Checking for existing recommendation cache...');\n \n const cachedRecommendation = await UserTeacherRecommendation.findValidRecommendation(\n userId, \n studentResponseData\n );\n \n if (cachedRecommendation && cachedRecommendation.isValid()) {\n console.log('✅ Found valid cached recommendation, returning cached data');\n \n // FIXED: Properly parse cached data if it's a string\n let cachedData = cachedRecommendation.recommendation_data;\n \n if (typeof cachedData === 'string') {\n try {\n cachedData = JSON.parse(cachedData);\n } catch (parseError) {\n console.error('Error parsing cached data:', parseError);\n // Continue with fresh generation if cache is corrupted\n }\n }\n \n // Convert timestamps for response\n const cacheInfo = {\n generatedAt: new Date(cachedRecommendation.created_at * 1000).toISOString(),\n expiresAt: cachedRecommendation.expires_at ? new Date(cachedRecommendation.expires_at * 1000).toISOString() : null,\n aiProvider: cachedRecommendation.ai_provider,\n aiStatus: cachedRecommendation.ai_status,\n totalTeachers: cachedRecommendation.total_teachers_count\n };\n \n return res.status(200).json({\n success: true,\n message: 'Cached teacher recommendations retrieved successfully',\n data: cachedData,\n cached: true,\n cacheInfo: cacheInfo\n });\n }\n \n console.log('❌ No valid cache found, generating new recommendations...');\n \n } catch (cacheError) {\n console.error('⚠️ Cache check failed, proceeding with generation:', cacheError);\n }\n } else {\n console.log('🔄 Force refresh requested, skipping cache...');\n }\n \n // Get ALL active teachers with comprehensive data (keeping your existing logic)\n let allTeachers = [];\n try {\n const basicTeachers = await Users.findAll({\n where: {\n role_name: 'teacher',\n status: 'active',\n ban: false,\n deleted_at: null\n },\n attributes: [\n 'id', 'full_name', 'email', 'bio', 'subject',\n 'education', 'experience', 'about', 'language', 'meeting_type',\n 'video_demo', 'video_demo_thumb', 'video_demo_source', \n 'headline', 'timezone', 'avatar'\n ],\n order: [['full_name', 'ASC']]\n });\n\n if (basicTeachers.length === 0) {\n return res.status(200).json({\n success: false,\n message: 'No active teachers available',\n data: null\n });\n }\n\n const teacherIds = basicTeachers.map(teacher => teacher.id);\n\n // Get ALL occupation data for teachers\n const teacherOccupations = await UserOccupation.findAll({\n where: {\n user_id: { [Op.in]: teacherIds },\n type: { [Op.in]: ['teachings', 'also_speaking', 'specialties', 'levels'] }\n },\n attributes: ['user_id', 'type', 'category_id', 'value'],\n order: [['user_id', 'ASC'], ['type', 'ASC']]\n });\n\n // Get teacher reviews\n const teacherReviews = await UserReview.findAll({\n where: {\n instructor_id: { [Op.in]: teacherIds }\n },\n attributes: ['instructor_id', 'rates', 'description']\n });\n\n // Organize data\n const occupationsByTeacher = {};\n teacherOccupations.forEach(occ => {\n if (!occupationsByTeacher[occ.user_id]) {\n occupationsByTeacher[occ.user_id] = {\n teachings: [],\n also_speaking: [],\n specialties: [],\n levels: []\n };\n }\n occupationsByTeacher[occ.user_id][occ.type].push(occ.value);\n });\n\n // Combine all teacher data\n allTeachers = basicTeachers.map(teacher => {\n const occupations = occupationsByTeacher[teacher.id] || {\n teachings: [],\n also_speaking: [],\n specialties: [],\n levels: []\n };\n\n const reviews = teacherReviews.filter(review => review.instructor_id === teacher.id);\n \n let averageRating = 0;\n let totalReviews = reviews.length;\n \n if (totalReviews > 0) {\n const ratesArray = reviews.map(review => parseFloat(review.rates));\n const totalRates = ratesArray.reduce((acc, rate) => acc + rate, 0);\n averageRating = (totalRates / totalReviews).toFixed(1);\n }\n\n return {\n id: teacher.id,\n name: teacher.full_name,\n email: teacher.email,\n avatar: teacher.avatar,\n bio: teacher.bio,\n subject: teacher.subject,\n education: teacher.education,\n experience: teacher.experience,\n about: teacher.about,\n language: teacher.language,\n meetingType: teacher.meeting_type,\n headline: teacher.headline,\n timezone: teacher.timezone,\n videoDemo: teacher.video_demo,\n videoDemoThumb: teacher.video_demo_thumb,\n videoDemoSource: teacher.video_demo_source,\n teachings: occupations.teachings,\n speakingLanguages: occupations.also_speaking,\n specialties: occupations.specialties,\n levels: occupations.levels,\n reviews: {\n totalReviews: totalReviews,\n averageRating: parseFloat(averageRating),\n recentReviews: reviews.slice(0, 3).map(review => ({\n overallRating: parseFloat(review.rates),\n description: review.description\n }))\n }\n };\n });\n\n console.log(`✅ Fetched ${allTeachers.length} total teachers`);\n\n } catch (error) {\n console.error('❌ Error fetching teacher data:', error);\n return res.status(500).json({\n success: false,\n message: 'Unable to fetch teacher information',\n data: null\n });\n }\n \n // 🔍 DEBUG: Track teacher flow\n console.log('🔍 TEACHER FLOW DEBUG:');\n console.log('1. Total teachers found:', allTeachers.length);\n console.log(' Teacher IDs:', allTeachers.map(t => t.id).join(', '));\n \n // SMART FILTERING: Get best teachers for AI processing\n const filterResult = filterBestTeachers(allTeachers, studentResponseData, 6);\n const { selectedTeachers, remainingTeachers, studentPreferences } = filterResult;\n \n console.log('2. After smart filtering:');\n console.log(' Selected for AI (top 6):', selectedTeachers.length);\n console.log(' Selected IDs:', selectedTeachers.map(t => t.id).join(', '));\n console.log(' Remaining teachers:', remainingTeachers.length);\n console.log(' Remaining IDs:', remainingTeachers.map(t => t.id).join(', '));\n \n if (selectedTeachers.length === 0) {\n return res.status(200).json({\n success: false,\n message: 'No suitable teachers found',\n data: null\n });\n }\n \n // AI FINAL RANKING: Get top 3 + next 3 from the selected teachers\n let aiRecommendation = null;\n let aiStatus = 'success';\n let aiMessage = 'Teacher recommendations generated successfully';\n let aiProvider = null;\n \n try {\n console.log('🤖 Starting AI final ranking for selected teachers...');\n \n const aiResult = await getFinalTeacherRanking(studentResponseData, selectedTeachers);\n \n if (aiResult.success) {\n aiRecommendation = aiResult.data;\n aiProvider = aiResult.provider;\n console.log(`✅ AI final ranking completed using ${aiResult.provider}`);\n } else {\n aiRecommendation = aiResult.fallbackRecommendation;\n aiStatus = 'fallback';\n aiMessage = 'Using fallback recommendations (AI temporarily unavailable)';\n aiProvider = 'Fallback';\n console.log('⚠️ Using fallback recommendation');\n }\n } catch (error) {\n console.error('❌ AI processing error:', error);\n aiRecommendation = createSimpleFallback(selectedTeachers);\n aiStatus = 'error';\n aiMessage = 'AI processing failed, using basic recommendations';\n aiProvider = 'Fallback';\n }\n \n if (!aiRecommendation) {\n return res.status(200).json({\n success: false,\n message: aiMessage,\n data: null\n });\n }\n\n console.log('3. After AI processing:');\n console.log(' AI returned topThreeTeachers:', aiRecommendation.topThreeTeachers?.length || 0);\n console.log(' AI returned nextThreeTeachers:', aiRecommendation.nextThreeTeachers?.length || 0);\n\n if (aiRecommendation.topThreeTeachers) {\n console.log(' Top 3 IDs:', aiRecommendation.topThreeTeachers.map(t => t.teacherId).join(', '));\n }\n if (aiRecommendation.nextThreeTeachers) {\n console.log(' Next 3 IDs:', aiRecommendation.nextThreeTeachers.map(t => t.teacherId).join(', '));\n }\n\n // Helper function to get full teacher data by ID\n const getFullTeacherData = (teacherId) => {\n const teacher = allTeachers.find(t => t.id === teacherId);\n if (!teacher) return null;\n \n return {\n teacherId: teacher.id,\n name: teacher.name,\n about: teacher.about,\n avatar: teacher.avatar,\n language: teacher.language,\n headline: teacher.headline,\n timezone: teacher.timezone,\n videoDemo: teacher.videoDemo,\n videoDemoThumb: teacher.videoDemoThumb,\n videoDemoSource: teacher.videoDemoSource,\n bio: teacher.bio,\n subject: teacher.subject,\n education: teacher.education,\n experience: teacher.experience,\n meetingType: teacher.meetingType,\n teachings: teacher.teachings,\n speakingLanguages: teacher.speakingLanguages,\n specialties: teacher.specialties,\n levels: teacher.levels,\n reviews: teacher.reviews,\n compatibilityScore: teacher.compatibilityScore || null\n };\n };\n \n // 🔧 BUILD FINAL RESPONSE WITH GUARANTEED ALL TEACHERS\n const responseData = {\n // Top 3 teachers (AI ranked)\n topThreeTeachers: aiRecommendation.topThreeTeachers?.map(teacher => {\n const fullTeacherData = getFullTeacherData(teacher.teacherId);\n return {\n ...fullTeacherData,\n matchScore: teacher.matchScore,\n matchReasons: teacher.matchReasons || []\n };\n }).filter(Boolean) || [],\n \n // Next 3 teachers (AI ranked or fallback)\n nextThreeTeachers: aiRecommendation.nextThreeTeachers?.map(teacher => {\n const fullTeacherData = getFullTeacherData(teacher.teacherId);\n return {\n ...fullTeacherData,\n matchScore: teacher.matchScore,\n matchReasons: teacher.matchReasons || []\n };\n }).filter(Boolean) || [],\n \n // 🆕 ALL REMAINING TEACHERS (ensures all teachers are included)\n otherMatchedTeachers: []\n };\n \n // Get teacher IDs that are already included in the response\n const topThreeIds = responseData.topThreeTeachers.map(t => t.teacherId);\n const nextThreeIds = responseData.nextThreeTeachers.map(t => t.teacherId);\n const includedIds = [...topThreeIds, ...nextThreeIds];\n \n // Find any teachers from the selected teachers that are missing\n const missingFromSelected = selectedTeachers.filter(teacher => \n !includedIds.includes(teacher.id)\n );\n \n // Combine missing teachers with remaining teachers for otherMatchedTeachers\n const allOtherTeachers = [...missingFromSelected, ...remainingTeachers];\n \n // Add all other teachers to the response\n responseData.otherMatchedTeachers = allOtherTeachers.map(teacher => ({\n teacherId: teacher.id,\n name: teacher.name,\n about: teacher.about,\n avatar: teacher.avatar,\n language: teacher.language,\n headline: teacher.headline,\n timezone: teacher.timezone,\n videoDemo: teacher.videoDemo,\n videoDemoThumb: teacher.videoDemoThumb,\n videoDemoSource: teacher.videoDemoSource,\n bio: teacher.bio,\n subject: teacher.subject,\n education: teacher.education,\n experience: teacher.experience,\n meetingType: teacher.meetingType,\n teachings: teacher.teachings,\n speakingLanguages: teacher.speakingLanguages,\n specialties: teacher.specialties,\n levels: teacher.levels,\n reviews: teacher.reviews,\n compatibilityScore: teacher.compatibilityScore || null,\n matchScore: 0,\n matchReasons: []\n }));\n \n // 📊 Calculate final summary\n const summary = {\n topThreeCount: responseData.topThreeTeachers.length,\n nextThreeCount: responseData.nextThreeTeachers.length,\n otherMatchedCount: responseData.otherMatchedTeachers.length,\n totalTeachersProcessed: allTeachers.length,\n totalTeachersReturned: responseData.topThreeTeachers.length + \n responseData.nextThreeTeachers.length + \n responseData.otherMatchedTeachers.length,\n aiProvider: aiProvider,\n aiStatus: aiStatus\n };\n \n console.log('4. Final response breakdown:');\n console.log(' topThreeTeachers:', summary.topThreeCount);\n console.log(' nextThreeTeachers:', summary.nextThreeCount);\n console.log(' otherMatchedTeachers:', summary.otherMatchedCount);\n console.log(' TOTAL RETURNED:', summary.totalTeachersReturned, '/', summary.totalTeachersProcessed);\n \n // Ensure we're returning all teachers\n if (summary.totalTeachersReturned !== summary.totalTeachersProcessed) {\n console.error(`⚠️ MISMATCH: Expected ${summary.totalTeachersProcessed} teachers, but returning ${summary.totalTeachersReturned}`);\n } else {\n console.log('✅ SUCCESS: All teachers included in response');\n }\n \n // 💾 NEW: SAVE TO CACHE\n try {\n console.log('💾 Saving recommendation to cache...');\n \n await UserTeacherRecommendation.createRecommendation(\n userId,\n studentResponseData,\n responseData,\n {\n totalTeachers: allTeachers.length,\n selectedTeachers: selectedTeachers.length,\n provider: aiProvider,\n status: aiStatus\n }\n );\n \n console.log('✅ Recommendation cached successfully');\n \n } catch (cacheError) {\n console.error('⚠️ Failed to cache recommendation:', cacheError);\n // Don't fail the request if caching fails\n }\n \n // API RESPONSE AS REQUESTED\n return res.status(200).json({\n success: true,\n message: aiMessage,\n data: responseData,\n cached: false,\n generated: true,\n summary: summary\n });\n \n } catch (err) {\n console.error('❌ Critical error in storeMultipleResponses:', err);\n return res.status(500).json({\n success: false,\n message: 'Internal server error occurred',\n data: null\n });\n }\n}\n\n// FIXED: Get cached recommendations only (with proper JSON parsing)\nasync function getCachedRecommendations(req, res) {\n try {\n const userId = req.userId;\n \n const cachedRecommendation = await UserTeacherRecommendation.findOne({\n where: {\n user_id: userId,\n is_active: true,\n deleted_at: null\n },\n order: [['created_at', 'DESC']]\n });\n \n if (!cachedRecommendation || !cachedRecommendation.isValid()) {\n return res.status(404).json({\n success: false,\n message: 'No valid cached recommendations found',\n data: null\n });\n }\n \n // FIXED: Properly parse the recommendation_data if it's a string\n let recommendationData = cachedRecommendation.recommendation_data;\n \n // If the data is still a string, parse it\n if (typeof recommendationData === 'string') {\n try {\n recommendationData = JSON.parse(recommendationData);\n } catch (parseError) {\n console.error('Error parsing cached recommendation data:', parseError);\n return res.status(500).json({\n success: false,\n message: 'Cached data format error',\n data: null\n });\n }\n }\n \n // Convert timestamps for response\n const cacheInfo = {\n generatedAt: new Date(cachedRecommendation.created_at * 1000).toISOString(),\n expiresAt: cachedRecommendation.expires_at ? new Date(cachedRecommendation.expires_at * 1000).toISOString() : null,\n aiProvider: cachedRecommendation.ai_provider,\n aiStatus: cachedRecommendation.ai_status,\n totalTeachers: cachedRecommendation.total_teachers_count\n };\n \n return res.status(200).json({\n success: true,\n message: 'Cached recommendations retrieved successfully',\n data: recommendationData, // NOW RETURNS PROPER JSON OBJECT\n cached: true,\n cacheInfo: cacheInfo\n });\n \n } catch (err) {\n console.error('Error fetching cached recommendations:', err);\n return res.status(500).json({\n success: false,\n message: 'Internal server error while fetching cached recommendations',\n data: null\n });\n }\n}\n\n// NEW: Clear user's recommendation cache\nasync function clearRecommendationCache(req, res) {\n try {\n const userId = req.userId;\n \n const affectedRows = await UserTeacherRecommendation.softDeleteUserRecommendations(userId);\n \n return res.status(200).json({\n success: true,\n message: `Cleared ${affectedRows} cached recommendations`,\n data: { clearedCount: affectedRows }\n });\n \n } catch (err) {\n console.error('Error clearing recommendation cache:', err);\n return res.status(500).json({\n success: false,\n message: 'Internal server error while clearing cache',\n data: null\n });\n }\n}\n\n// Get user's responses to questions\nasync function getUserResponses(req, res) {\n try {\n const userId = req.userId;\n const { questionId } = req.query;\n \n const whereCondition = { user_id: userId };\n \n // If specific question ID is provided\n if (questionId) {\n whereCondition.question_id = questionId;\n }\n \n const responses = await UserQuestionResponse.findAll({\n where: whereCondition,\n include: [\n {\n model: QuestionBank,\n as: 'question',\n attributes: ['id', 'question', 'type', 'options', 'question_order']\n }\n ],\n order: [['created_at', 'DESC']]\n });\n \n if (!responses || responses.length === 0) {\n return res.status(404).json({\n status: 'error',\n message: 'No responses found'\n });\n }\n \n // Format responses for mobile\n const formattedResponses = responses.map(response => ({\n id: response.id,\n questionId: response.question_id,\n responseText: response.response_text,\n selectedOptions: response.selected_options,\n questionType: response.question_type,\n createdAt: response.created_at,\n updatedAt: response.updated_at,\n question: response.question ? {\n id: response.question.id,\n text: response.question.question,\n type: response.question.type,\n order: response.question.question_order\n } : null\n }));\n \n return res.status(200).json({\n status: 'success',\n message: 'User responses retrieved successfully',\n count: formattedResponses.length,\n data: formattedResponses\n });\n \n } catch (err) {\n console.error('Error fetching user responses:', err);\n return res.status(500).json({\n status: 'error',\n message: 'Internal server error while fetching responses'\n });\n }\n}\n\n// Helper function to validate response based on question type\nfunction validateResponse(questionType, responseText, selectedOptions) {\n switch (questionType) {\n case 'text':\n if (!responseText || responseText.trim() === '') {\n return { isValid: false, message: 'Response text is required for text questions' };\n }\n return { isValid: true };\n \n case 'single-choice':\n case 'yes-no':\n if (!selectedOptions || !Array.isArray(selectedOptions) || selectedOptions.length !== 1) {\n return { isValid: false, message: `Exactly one option must be selected for ${questionType} questions` };\n }\n return { isValid: true };\n \n case 'multiple-choice':\n case 'checkbox':\n if (!selectedOptions || !Array.isArray(selectedOptions) || selectedOptions.length === 0) {\n return { isValid: false, message: `At least one option must be selected for ${questionType} questions` };\n }\n return { isValid: true };\n \n default:\n return { isValid: false, message: 'Invalid question type' };\n }\n}\n\n// MODULE EXPORTS - COMPLETE LIST INCLUDING NEW FUNCTION\nmodule.exports = {\n getAllActiveQuestions,\n getQuestionsByType,\n storeQuestionResponse,\n storeMultipleResponses, // Enhanced with caching and fixed teacher distribution\n getUserResponses,\n getCachedRecommendations,\n clearRecommendationCache,\n getUserQuestionnaireStatus // 🆕 NEW: Your main function for checking status\n};"}, "edb51054353c2300d4c03614a2b78229595ea401": {"t": 1785754178.7850454, "n": 70956, "h": "0bc81533e5941168", "c": "// Complete questionBank.controller.js with FIXED teacher distribution\n\nconst bcrypt = require('bcrypt');\nconst Joi = require('joi');\nconst axios = require('axios');\n\nconst Users = require('../../models/users');\nconst QuestionBank = require('../../models/questionBank');\nconst UserQuestionResponse = require('../../models/userQuestionResponse');\nconst UserTeacherRecommendation = require('../../models/userTeacherRecommendation');\nconst e = require('cors');\nconst { DataTypes, Op } = require('sequelize');\nconst UserOccupation = require('../../models/usersOccupation');\nconst UserReview = require('../../models/userReviews');\n\n// AI API Configuration\nconst DEEPSEEK_API_KEY = 'sk-0500f22e4577455f959763dabba7ae43';\nconst OPENAI_API_KEY = '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';\n\nconst DEEPSEEK_API_URL = 'https://api.deepseek.com/chat/completions';\nconst OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions';\n\n// Get all active questions ordered by question_order\nasync function getAllActiveQuestions(req, res) {\n try {\n // Get language from query parameter, default to 'en'\n const language = req.query.language || 'en';\n \n // Fetch all active questions ordered by question_order then created_at\n const questions = await QuestionBank.findAll({\n where: { \n is_active: 1 \n },\n attributes: ['id', 'question', 'type', 'options', 'question_order', 'created_at'],\n order: [['question_order', 'ASC'], ['created_at', 'ASC']]\n });\n\n if (!questions || questions.length === 0) {\n return res.status(404).json({ \n status: 'error', \n message: 'No active questions found' \n });\n }\n\n // Format questions for mobile response with localization\n const formattedQuestions = questions.map(question => {\n return {\n id: question.id,\n question: question.getLocalizedQuestion(language),\n type: question.type,\n options: question.getLocalizedOptions(language),\n order: question.question_order,\n created_at: question.created_at\n };\n });\n\n return res.status(200).json({ \n status: 'success', \n message: 'Questions retrieved successfully',\n count: formattedQuestions.length,\n data: formattedQuestions \n });\n\n } catch (err) {\n console.error('Error fetching questions:', err);\n return res.status(500).json({\n status: 'error',\n message: 'Internal server error while fetching questions'\n });\n }\n}\n\n// Get questions by type ordered by question_order\nasync function getQuestionsByType(req, res) {\n try {\n const { type } = req.params;\n const language = req.query.language || 'en';\n \n // Validate question type\n const validTypes = ['single-choice', 'multiple-choice', 'checkbox', 'yes-no', 'text'];\n if (!validTypes.includes(type)) {\n return res.status(400).json({\n status: 'error',\n message: `Invalid question type. Valid types are: ${validTypes.join(', ')}`\n });\n }\n\n // Fetch questions by type ordered by question_order then created_at\n const questions = await QuestionBank.findAll({\n where: { \n is_active: 1,\n type: type\n },\n attributes: ['id', 'question', 'type', 'options', 'question_order', 'created_at'],\n order: [['question_order', 'ASC'], ['created_at', 'ASC']]\n });\n\n if (!questions || questions.length === 0) {\n return res.status(404).json({ \n status: 'error', \n message: `No active questions found for type: ${type}` \n });\n }\n\n // Format questions for mobile response with localization\n const formattedQuestions = questions.map(question => {\n return {\n id: question.id,\n question: question.getLocalizedQuestion(language),\n type: question.type,\n options: question.getLocalizedOptions(language),\n order: question.question_order,\n created_at: question.created_at\n };\n });\n\n return res.status(200).json({ \n status: 'success', \n message: `Questions of type '${type}' retrieved successfully`,\n count: formattedQuestions.length,\n data: formattedQuestions \n });\n\n } catch (err) {\n console.error('Error fetching questions by type:', err);\n return res.status(500).json({\n status: 'error',\n message: 'Internal server error while fetching questions'\n });\n }\n}\n\n// Helper function to validate if selected options exist in question options\nfunction validateSelectedOptions(selectedOptions, questionOptions, questionType) {\n if (questionType === 'text') {\n return { isValid: true };\n }\n \n if (!selectedOptions || !Array.isArray(selectedOptions)) {\n return { isValid: false, message: 'Selected options must be an array' };\n }\n \n if (!questionOptions || !Array.isArray(questionOptions)) {\n return { isValid: false, message: 'Question options not found' };\n }\n \n // Check if all selected options exist in question options\n for (const selectedOption of selectedOptions) {\n if (!questionOptions.includes(selectedOption)) {\n return { \n isValid: false, \n message: `Selected option \"${selectedOption}\" is not valid for this question` \n };\n }\n }\n \n return { isValid: true };\n}\n\n// Store user response to a question - UPDATED TO ACCEPT ACTUAL VALUES\nasync function storeQuestionResponse(req, res) {\n try {\n const { questionId, responseText, selectedOptions } = req.body;\n const userId = req.userId; // From auth middleware\n const language = req.query.language || 'en';\n \n // Validate required fields\n if (!questionId) {\n return res.status(400).json({\n status: 'error',\n message: 'Question ID is required'\n });\n }\n \n // Check if question exists and is active\n const question = await QuestionBank.findOne({\n where: {\n id: questionId,\n is_active: 1\n }\n });\n \n if (!question) {\n return res.status(404).json({\n status: 'error',\n message: 'Question not found or inactive'\n });\n }\n \n // Validate response based on question type\n const validationResult = validateResponse(question.type, responseText, selectedOptions);\n if (!validationResult.isValid) {\n return res.status(400).json({\n status: 'error',\n message: validationResult.message\n });\n }\n \n // For choice questions, validate that selected options exist in question options\n if (question.type !== 'text' && selectedOptions && Array.isArray(selectedOptions)) {\n const questionOptions = question.getLocalizedOptions(language);\n const optionValidation = validateSelectedOptions(selectedOptions, questionOptions, question.type);\n if (!optionValidation.isValid) {\n return res.status(400).json({\n status: 'error',\n message: optionValidation.message\n });\n }\n }\n \n // Check if user already responded to this question\n const existingResponse = await UserQuestionResponse.findOne({\n where: {\n user_id: userId,\n question_id: questionId\n }\n });\n \n let userResponse;\n \n if (existingResponse) {\n // Update existing response\n userResponse = await existingResponse.update({\n response_text: responseText || null,\n selected_options: selectedOptions || null, // Store actual values directly\n question_type: question.type,\n updated_at: new Date()\n });\n } else {\n // Create new response\n userResponse = await UserQuestionResponse.create({\n user_id: userId,\n question_id: questionId,\n response_text: responseText || null,\n selected_options: selectedOptions || null, // Store actual values directly\n question_type: question.type\n });\n }\n \n // Format response for mobile\n const formattedResponse = {\n id: userResponse.id,\n questionId: userResponse.question_id,\n responseText: userResponse.response_text,\n selectedOptions: userResponse.selected_options,\n questionType: userResponse.question_type,\n createdAt: userResponse.created_at,\n updatedAt: userResponse.updated_at\n };\n \n return res.status(200).json({\n status: 'success',\n message: existingResponse ? 'Response updated successfully' : 'Response stored successfully',\n data: formattedResponse\n });\n \n } catch (err) {\n console.error('Error storing question response:', err);\n return res.status(500).json({\n status: 'error',\n message: 'Internal server error while storing response'\n });\n }\n}\n\n// 🆕 NEW FUNCTION: Check user's questionnaire completion status\nasync function getUserQuestionnaireStatus(req, res) {\n try {\n const userId = req.userId; // From auth middleware\n const language = req.query.language || 'en';\n \n // Get all active questions count\n const totalActiveQuestions = await QuestionBank.count({\n where: { \n is_active: 1 \n }\n });\n \n if (totalActiveQuestions === 0) {\n return res.status(200).json({\n success: true,\n message: 'No active questions available',\n data: {\n status: 'no_questions',\n shouldShowQuestionnaire: false,\n hasCompletedQuestionnaire: false,\n hasCachedRecommendations: false,\n completionPercentage: 0,\n totalQuestions: 0,\n answeredQuestions: 0,\n action: 'no_action',\n lastAnsweredAt: null\n }\n });\n }\n \n // Get user's responses (only for active questions)\n const userResponses = await UserQuestionResponse.findAll({\n where: {\n user_id: userId\n },\n attributes: ['question_id', 'created_at'],\n include: [\n {\n model: QuestionBank,\n as: 'question', // This requires the association to be set up\n where: { is_active: 1 },\n required: true,\n attributes: ['id']\n }\n ]\n });\n \n const answeredQuestions = userResponses.length;\n const completionPercentage = Math.round((answeredQuestions / totalActiveQuestions) * 100);\n \n // Check if user has cached recommendations\n let hasCachedRecommendations = false;\n try {\n const cachedRecommendation = await UserTeacherRecommendation.findOne({\n where: {\n user_id: userId,\n is_active: true,\n deleted_at: null\n },\n order: [['created_at', 'DESC']]\n });\n \n hasCachedRecommendations = cachedRecommendation && cachedRecommendation.isValid();\n } catch (cacheError) {\n console.log('Cache check failed:', cacheError);\n hasCachedRecommendations = false;\n }\n \n // Determine user status and required action\n let status, shouldShowQuestionnaire, hasCompletedQuestionnaire, action, message;\n \n if (answeredQuestions === 0) {\n // User hasn't answered any questions\n status = 'not_started';\n shouldShowQuestionnaire = true;\n hasCompletedQuestionnaire = false;\n action = 'show_questionnaire';\n message = 'User needs to complete the questionnaire';\n \n } else if (answeredQuestions < totalActiveQuestions) {\n // 🎯 YOUR USE CASE: User has partially completed questionnaire\n status = 'partially_completed';\n shouldShowQuestionnaire = true;\n hasCompletedQuestionnaire = false;\n action = 'continue_questionnaire';\n message = `User has answered ${answeredQuestions} out of ${totalActiveQuestions} questions`;\n \n } else if (answeredQuestions === totalActiveQuestions) {\n // User has completed all questions\n hasCompletedQuestionnaire = true;\n \n if (hasCachedRecommendations) {\n // User has completed questionnaire and has cached recommendations\n status = 'completed_with_cache';\n shouldShowQuestionnaire = false;\n action = 'show_recommendations';\n message = 'User has completed questionnaire and has cached recommendations';\n } else {\n // User completed questionnaire but no cached recommendations\n status = 'completed_no_cache';\n shouldShowQuestionnaire = false;\n action = 'generate_recommendations';\n message = 'User completed questionnaire but needs to generate recommendations';\n }\n } else {\n // Edge case: user has more responses than active questions (old responses)\n status = 'over_completed';\n shouldShowQuestionnaire = false;\n hasCompletedQuestionnaire = true;\n action = hasCachedRecommendations ? 'show_recommendations' : 'generate_recommendations';\n message = 'User has responses but may need to update questionnaire';\n }\n \n return res.status(200).json({\n success: true,\n message: message,\n data: {\n status: status, // 'not_started', 'partially_completed', 'completed_with_cache', 'completed_no_cache', 'over_completed'\n shouldShowQuestionnaire: shouldShowQuestionnaire, // 🎯 YOUR MAIN BOOLEAN CHECK\n hasCompletedQuestionnaire: hasCompletedQuestionnaire,\n hasCachedRecommendations: hasCachedRecommendations,\n completionPercentage: completionPercentage, // 0-100: percentage of questions completed\n totalQuestions: totalActiveQuestions,\n answeredQuestions: answeredQuestions,\n action: action, // 'show_questionnaire', 'continue_questionnaire', 'show_recommendations', 'generate_recommendations'\n lastAnsweredAt: userResponses.length > 0 ? \n Math.max(...userResponses.map(r => new Date(r.created_at).getTime())) : null\n }\n });\n \n } catch (err) {\n console.error('Error checking user questionnaire status:', err);\n return res.status(500).json({\n success: false,\n message: 'Internal server error while checking questionnaire status',\n data: {\n status: 'error',\n shouldShowQuestionnaire: true, // Default to showing questionnaire on error\n hasCompletedQuestionnaire: false,\n hasCachedRecommendations: false,\n completionPercentage: 0,\n totalQuestions: 0,\n answeredQuestions: 0,\n action: 'show_questionnaire',\n lastAnsweredAt: null\n }\n });\n }\n}\n\n// SMART TEACHER PRE-FILTERING FUNCTIONS\nfunction extractStudentPreferences(studentResponses) {\n const preferences = {\n languages: [],\n subjects: [],\n level: 'intermediate',\n goals: [],\n preferredTeachingStyle: [],\n schedulePreferences: []\n };\n \n studentResponses.forEach(response => {\n const questionText = response.questionText.toLowerCase();\n \n // Extract language preferences\n if (questionText.includes('language') || questionText.includes('speak')) {\n if (response.selectedOptions && response.selectedOptions.length > 0) {\n preferences.languages.push(...response.selectedOptions);\n }\n }\n \n // Extract subject interests\n if (questionText.includes('subject') || questionText.includes('topic') || questionText.includes('focus')) {\n if (response.selectedOptions && response.selectedOptions.length > 0) {\n preferences.subjects.push(...response.selectedOptions);\n }\n }\n \n // Extract proficiency level - FIXED: Now expects actual text values\n if (questionText.includes('level') || questionText.includes('proficiency')) {\n if (response.selectedOptions && response.selectedOptions.length > 0) {\n console.log('response :', response);\n \n // Now we expect actual text values, not indices\n const levelOption = String(response.selectedOptions[0]).toLowerCase();\n \n if (levelOption.includes('beginner') || levelOption.includes('a1') || levelOption.includes('a2')) {\n preferences.level = 'beginner';\n } else if (levelOption.includes('advanced') || levelOption.includes('c1') || levelOption.includes('c2')) {\n preferences.level = 'advanced';\n } else {\n preferences.level = 'intermediate';\n }\n }\n }\n \n // Extract learning goals\n if (questionText.includes('goal') || questionText.includes('purpose') || questionText.includes('why')) {\n if (response.selectedOptions && response.selectedOptions.length > 0) {\n preferences.goals.push(...response.selectedOptions);\n }\n }\n });\n \n return preferences;\n}\n\nasync function convertIndicesToOptionValues(responses, language = 'en') {\n try {\n const questionIds = [...new Set(responses.map(r => r.questionId))];\n \n // Fetch question data including options\n const questions = await QuestionBank.findAll({\n where: {\n id: { [Op.in]: questionIds },\n is_active: 1\n },\n attributes: ['id', 'question', 'type', 'options', 'question_order']\n });\n \n const questionMap = {};\n questions.forEach(q => {\n questionMap[q.id] = {\n text: q.getLocalizedQuestion(language),\n type: q.type,\n options: q.getLocalizedOptions(language), // Get actual option text array\n order: q.question_order\n };\n });\n \n // Convert responses with indices to actual option values\n const convertedResponses = responses.map(response => {\n const questionInfo = questionMap[response.questionId];\n \n if (!questionInfo) {\n console.log(`⚠️ Question ${response.questionId} not found`);\n return {\n questionId: response.questionId,\n questionText: `Question ${response.questionId}`,\n questionType: response.responseText ? 'text' : 'choice',\n responseText: response.responseText,\n selectedOptions: response.selectedOptions,\n order: response.questionId\n };\n }\n \n let convertedSelectedOptions = null;\n \n // Convert indices to actual option text if needed\n if (response.selectedOptions && Array.isArray(response.selectedOptions) && questionInfo.options) {\n convertedSelectedOptions = response.selectedOptions.map(optionValue => {\n // Check if it's an index (number) that needs conversion\n if (typeof optionValue === 'number' && questionInfo.options[optionValue] !== undefined) {\n const actualValue = questionInfo.options[optionValue];\n console.log(`🔧 Question ${response.questionId}: Converted index ${optionValue} to \"${actualValue}\"`);\n return actualValue;\n }\n // If it's already a string or invalid index, return as-is\n return String(optionValue);\n });\n } else {\n convertedSelectedOptions = response.selectedOptions;\n }\n \n return {\n questionId: response.questionId,\n questionText: questionInfo.text,\n questionType: questionInfo.type,\n responseText: response.responseText,\n selectedOptions: convertedSelectedOptions,\n order: questionInfo.order\n };\n }).sort((a, b) => a.order - b.order);\n \n return convertedResponses;\n \n } catch (error) {\n console.error('❌ Error converting indices to option values:', error);\n \n // Fallback: return responses as-is with basic formatting\n return responses.map(response => ({\n questionId: response.questionId,\n questionText: `Question ${response.questionId}`,\n questionType: response.responseText ? 'text' : 'choice',\n responseText: response.responseText,\n selectedOptions: response.selectedOptions,\n order: response.questionId\n }));\n }\n}\n\nfunction calculateTeacherScore(teacher, studentPreferences) {\n let score = 0;\n let maxScore = 0;\n \n // 1. Language compatibility (25 points)\n maxScore += 25;\n if (teacher.speakingLanguages && teacher.speakingLanguages.length > 0) {\n const languageMatch = studentPreferences.languages.some(lang => \n teacher.speakingLanguages.some(teacherLang => \n teacherLang.toLowerCase().includes(lang.toLowerCase()) ||\n lang.toLowerCase().includes(teacherLang.toLowerCase())\n )\n );\n if (languageMatch) score += 25;\n else if (teacher.speakingLanguages.includes('English')) score += 15; // English as fallback\n }\n \n // 2. Subject/Specialty match (20 points)\n maxScore += 20;\n if (teacher.specialties && teacher.specialties.length > 0) {\n const subjectMatch = studentPreferences.subjects.some(subject =>\n teacher.specialties.some(specialty =>\n specialty.toLowerCase().includes(subject.toLowerCase()) ||\n subject.toLowerCase().includes(specialty.toLowerCase())\n )\n );\n if (subjectMatch) score += 20;\n \n // Also check teachings\n if (teacher.teachings && teacher.teachings.length > 0) {\n const teachingMatch = studentPreferences.subjects.some(subject =>\n teacher.teachings.some(teaching =>\n teaching.toLowerCase().includes(subject.toLowerCase()) ||\n subject.toLowerCase().includes(teaching.toLowerCase())\n )\n );\n if (teachingMatch) score += 10; // Bonus for teaching match\n }\n }\n \n // 3. Teaching level appropriateness (15 points)\n maxScore += 15;\n if (teacher.levels && teacher.levels.length > 0) {\n const levelMap = {\n 'beginner': ['a1', 'a2', 'beginner', 'basic'],\n 'intermediate': ['b1', 'b2', 'intermediate', 'general'],\n 'advanced': ['c1', 'c2', 'advanced', 'business', 'academic']\n };\n \n const appropriateLevels = levelMap[studentPreferences.level] || [];\n const levelMatch = teacher.levels.some(level =>\n appropriateLevels.some(appLevel =>\n level.toLowerCase().includes(appLevel)\n )\n );\n if (levelMatch) score += 15;\n }\n \n // 4. Review quality (20 points)\n maxScore += 20;\n if (teacher.reviews && teacher.reviews.totalReviews > 0) {\n const rating = parseFloat(teacher.reviews.averageRating);\n if (rating >= 4.5) score += 20;\n else if (rating >= 4.0) score += 15;\n else if (rating >= 3.5) score += 10;\n else if (rating >= 3.0) score += 5;\n \n // Bonus for having multiple reviews\n if (teacher.reviews.totalReviews >= 10) score += 5;\n else if (teacher.reviews.totalReviews >= 5) score += 3;\n }\n \n // 5. Profile completeness (10 points)\n maxScore += 10;\n let completenessScore = 0;\n if (teacher.headline && teacher.headline.length > 10) completenessScore += 2;\n if (teacher.bio && teacher.bio.length > 50) completenessScore += 2;\n if (teacher.videoDemo) completenessScore += 3;\n if (teacher.education && teacher.education.length > 5) completenessScore += 1;\n if (teacher.experience && teacher.experience.length > 5) completenessScore += 1;\n if (teacher.about && teacher.about.length > 20) completenessScore += 1;\n score += completenessScore;\n \n // 6. Teaching variety (10 points)\n maxScore += 10;\n const totalTeachingAreas = (teacher.teachings ? teacher.teachings.length : 0) +\n (teacher.specialties ? teacher.specialties.length : 0);\n if (totalTeachingAreas >= 5) score += 10;\n else if (totalTeachingAreas >= 3) score += 7;\n else if (totalTeachingAreas >= 1) score += 4;\n \n // Return percentage score\n return maxScore > 0 ? Math.round((score / maxScore) * 100) : 0;\n}\n\nfunction filterBestTeachers(allTeachers, studentResponses, limit = 6) {\n console.log(`🎯 Starting smart filtering from ${allTeachers.length} teachers...`);\n \n // Extract student preferences\n const studentPreferences = extractStudentPreferences(studentResponses);\n console.log('📊 Student preferences:', studentPreferences);\n \n // Score all teachers\n const scoredTeachers = allTeachers.map(teacher => {\n const score = calculateTeacherScore(teacher, studentPreferences);\n return {\n ...teacher,\n compatibilityScore: score\n };\n });\n \n // Sort by score and take top teachers\n const sortedTeachers = scoredTeachers.sort((a, b) => {\n // Primary sort by compatibility score\n if (b.compatibilityScore !== a.compatibilityScore) {\n return b.compatibilityScore - a.compatibilityScore;\n }\n \n // Secondary sort by review rating\n const aRating = a.reviews?.averageRating || 0;\n const bRating = b.reviews?.averageRating || 0;\n if (bRating !== aRating) {\n return bRating - aRating;\n }\n \n // Tertiary sort by number of reviews\n const aReviews = a.reviews?.totalReviews || 0;\n const bReviews = b.reviews?.totalReviews || 0;\n return bReviews - aReviews;\n });\n \n const selectedTeachers = sortedTeachers.slice(0, limit);\n const remainingTeachers = sortedTeachers.slice(limit);\n \n console.log(`✅ Selected top ${selectedTeachers.length} teachers with scores:`,\n selectedTeachers.map(t => `${t.name} (${t.compatibilityScore}%)`).join(', ')\n );\n \n return {\n selectedTeachers,\n remainingTeachers,\n studentPreferences\n };\n}\n\n// AI RECOMMENDATION FUNCTIONS (Modified for 6 teachers only)\nasync function callDeepSeekAPI(prompt) {\n try {\n console.log('🤖 Calling DeepSeek API...');\n \n const response = await axios.post(DEEPSEEK_API_URL, {\n model: \"deepseek-chat\",\n messages: [\n {\n role: \"system\",\n content: \"You are an expert teacher matching system. Analyze student responses and pre-selected teacher profiles to provide final ranking. Always respond with valid JSON format.\"\n },\n {\n role: \"user\",\n content: prompt\n }\n ],\n max_tokens: 1500,\n temperature: 0.7\n }, {\n headers: {\n 'Authorization': `Bearer ${DEEPSEEK_API_KEY}`,\n 'Content-Type': 'application/json'\n },\n timeout: 30000\n });\n \n console.log('✅ DeepSeek API response received');\n return {\n success: true,\n data: response.data.choices[0].message.content,\n provider: 'DeepSeek'\n };\n \n } catch (error) {\n console.error('❌ DeepSeek API Error:', error.response?.data || error.message);\n return {\n success: false,\n error: error.response?.data?.error?.message || error.message,\n provider: 'DeepSeek'\n };\n }\n}\n\nasync function callOpenAI(prompt) {\n try {\n console.log('🤖 Calling OpenAI API (fallback)...');\n \n const response = await axios.post(OPENAI_API_URL, {\n model: \"gpt-3.5-turbo\",\n messages: [\n {\n role: \"system\",\n content: \"You are an expert teacher matching system. Analyze student responses and pre-selected teacher profiles to provide final ranking. Always respond with valid JSON format.\"\n },\n {\n role: \"user\",\n content: prompt\n }\n ],\n max_tokens: 1500,\n temperature: 0.7\n }, {\n headers: {\n 'Authorization': `Bearer ${OPENAI_API_KEY}`,\n 'Content-Type': 'application/json'\n },\n timeout: 30000\n });\n \n console.log('✅ OpenAI API response received');\n return {\n success: true,\n data: response.data.choices[0].message.content,\n provider: 'OpenAI'\n };\n \n } catch (error) {\n console.error('❌ OpenAI API Error:', error.response?.data || error.message);\n return {\n success: false,\n error: error.response?.data?.error?.message || error.message,\n provider: 'OpenAI'\n };\n }\n}\n\n// 🔧 UPDATED: getFinalTeacherRanking with fallback for missing nextThreeTeachers\nasync function getFinalTeacherRanking(studentResponses, selectedTeachers) {\n const prompt = `\nAnalyze this student's learning profile and provide final ranking for these ${selectedTeachers.length} PRE-SELECTED teachers:\n\nSTUDENT PROFILE:\n${studentResponses.map(response => {\n if (response.selectedOptions) {\n return `Question: ${response.questionText}\nAnswer: ${JSON.stringify(response.selectedOptions)}`;\n } else {\n return `Question: ${response.questionText}\nAnswer: \"${response.responseText}\"`;\n }\n}).join('\\n\\n')}\n\n${selectedTeachers.length} PRE-SELECTED TEACHERS (already filtered for basic compatibility):\n${selectedTeachers.map((teacher, index) => {\n const teachingsText = teacher.teachings && teacher.teachings.length > 0 \n ? teacher.teachings.join(', ') \n : 'General English';\n \n const languagesText = teacher.speakingLanguages && teacher.speakingLanguages.length > 0 \n ? teacher.speakingLanguages.join(', ') \n : teacher.language || 'English';\n \n const specialtiesText = teacher.specialties && teacher.specialties.length > 0 \n ? teacher.specialties.join(', ') \n : 'General Teaching';\n \n const levelsText = teacher.levels && teacher.levels.length > 0 \n ? teacher.levels.join(', ') \n : 'All levels';\n \n const reviewSummary = teacher.reviews && teacher.reviews.totalReviews > 0\n ? `${teacher.reviews.totalReviews} reviews (${teacher.reviews.averageRating}/5.0)`\n : 'No reviews yet';\n \n const recentReviewsText = teacher.reviews && teacher.reviews.recentReviews.length > 0\n ? teacher.reviews.recentReviews.map(review => \n `\"${review.description}\" (${review.overallRating}/5)`\n ).join('; ')\n : 'No detailed feedback';\n\n return `Teacher ${index + 1}:\n - ID: ${teacher.id}\n - Name: ${teacher.name}\n - Headline: ${teacher.headline || 'Experienced Teacher'}\n - Bio: ${teacher.bio || 'Dedicated to helping students achieve their goals'}\n - Experience: ${teacher.experience || 'Professional teaching experience'}\n - Compatibility Score: ${teacher.compatibilityScore}%\n \n TEACHING PROFILE:\n - Teaches: ${teachingsText}\n - Languages: ${languagesText}\n - Specializations: ${specialtiesText}\n - Levels: ${levelsText}\n \n STUDENT FEEDBACK:\n - Reviews: ${reviewSummary}\n - Recent Comments: ${recentReviewsText}\n \n EXTRAS:\n - Video Demo: ${teacher.videoDemo ? 'Available' : 'Not available'}\n - Timezone: ${teacher.timezone || 'Flexible'}`;\n }).join('\\n\\n')}\n\n RANKING INSTRUCTIONS:\n These ${selectedTeachers.length} teachers are already pre-filtered for basic compatibility. Your job is to provide nuanced final ranking based on:\n\n 1. **Deep Teaching Match**: How well does their teaching style/approach fit student's specific learning goals?\n 2. **Communication Fit**: Language comfort level and teaching personality match\n 3. **Experience Relevance**: Specific experience in areas student wants to improve\n 4. **Student Success Track**: Quality of student feedback and teaching effectiveness\n 5. **Learning Style Alignment**: Teaching methods that match student's preferred learning approach\n\n RESPONSE REQUIREMENTS:\n - You MUST rank all ${selectedTeachers.length} teachers\n - Top 3 = Best matches (topThreeTeachers) - positions 1-3\n - Next 3 = Good alternatives (nextThreeTeachers) - positions 4-6 \n - Provide detailed, specific match reasons (2-3 per teacher)\n - Include match scores (85-99 for top 3, 70-84 for next 3)\n\n Return ONLY valid JSON (no extra text) in this UPDATED format:\n {\n \"topThreeTeachers\": [\n {\n \"teacherId\": 123,\n \"name\": \"Teacher Name\",\n \"matchScore\": 95,\n \"matchReasons\": [\n \"Specializes in exactly what student needs (Business English)\",\n \"Excellent student feedback with 4.8/5 rating from 25+ students\",\n \"Teaching style matches student's preference for interactive learning\"\n ]\n }\n ],\n \"nextThreeTeachers\": [\n {\n \"teacherId\": 456,\n \"name\": \"Teacher Name\", \n \"matchScore\": 78,\n \"matchReasons\": [\n \"Good general English experience with solid 4.2/5 rating\",\n \"Available for student's preferred schedule\",\n \"Professional approach with video demo\"\n ]\n }\n ],\n \"analysis\": {\n \"studentLevel\": \"Intermediate\",\n \"primaryGoals\": [\"Business English\", \"Speaking Confidence\"],\n \"keyPreferences\": [\"Interactive lessons\", \"Native speaker\", \"Evening availability\"],\n \"rankingSummary\": \"Prioritized teachers with business English expertise and interactive teaching methods based on student's professional goals\"\n }\n }`;\n\n // Try DeepSeek first, then OpenAI as fallback\n let aiResponse = await callDeepSeekAPI(prompt);\n \n if (!aiResponse.success) {\n console.log('🔄 DeepSeek failed, trying OpenAI...');\n aiResponse = await callOpenAI(prompt);\n }\n \n if (!aiResponse.success) {\n console.error('❌ Both AI APIs failed');\n return {\n success: false,\n error: 'Both DeepSeek and OpenAI APIs failed',\n fallbackRecommendation: createSimpleFallback(selectedTeachers)\n };\n }\n \n try {\n // Clean up and parse JSON response\n let responseText = aiResponse.data.trim();\n \n console.log('📝 Raw AI Response:', responseText.substring(0, 200) + '...');\n \n // Remove markdown code block markers if present\n if (responseText.startsWith('```json')) {\n responseText = responseText.replace(/^```json\\s*/, '').replace(/\\s*```$/, '');\n }\n \n // Find the actual JSON content\n const jsonStart = responseText.indexOf('{');\n const jsonEnd = responseText.lastIndexOf('}') + 1;\n \n if (jsonStart !== -1 && jsonEnd > jsonStart) {\n responseText = responseText.substring(jsonStart, jsonEnd);\n }\n \n const parsedResponse = JSON.parse(responseText);\n \n // Validate response structure\n if (!parsedResponse.topThreeTeachers) {\n throw new Error('Invalid response structure - missing topThreeTeachers');\n }\n \n // 🆕 NEW: Handle missing or incomplete nextThreeTeachers\n if (!parsedResponse.nextThreeTeachers || parsedResponse.nextThreeTeachers.length === 0) {\n console.log('⚠️ AI did not return nextThreeTeachers, creating fallback...');\n \n // Get teacher IDs that are already in topThreeTeachers\n const topThreeIds = parsedResponse.topThreeTeachers.map(t => t.teacherId);\n \n // Find remaining teachers from the selected teachers\n const remainingFromSelected = selectedTeachers.filter(teacher => \n !topThreeIds.includes(teacher.id)\n );\n \n // Create nextThreeTeachers from remaining teachers\n parsedResponse.nextThreeTeachers = remainingFromSelected.slice(0, 3).map((teacher, index) => ({\n teacherId: teacher.id,\n name: teacher.name,\n matchScore: 75 - (index * 3), // Decreasing scores: 75, 72, 69\n matchReasons: [\n 'Good compatibility with student profile',\n teacher.reviews?.totalReviews > 0 \n ? `Rated ${teacher.reviews.averageRating}/5 by ${teacher.reviews.totalReviews} students`\n : 'Professional teaching experience',\n 'Available for lessons'\n ]\n }));\n \n console.log('✅ Created fallback nextThreeTeachers:', \n parsedResponse.nextThreeTeachers.map(t => `${t.name} (${t.matchScore}%)`).join(', ')\n );\n }\n \n console.log('✅ Successfully parsed AI ranking');\n console.log('🥇 Top teachers:', parsedResponse.topThreeTeachers?.map(t => `${t.name} (${t.matchScore}%)`).join(', '));\n console.log('🥈 Next teachers:', parsedResponse.nextThreeTeachers?.map(t => `${t.name} (${t.matchScore}%)`).join(', '));\n \n return {\n success: true,\n data: parsedResponse,\n provider: aiResponse.provider\n };\n \n } catch (parseError) {\n console.error('❌ Failed to parse AI response:', parseError);\n \n return {\n success: false,\n error: 'Failed to parse AI response',\n fallbackRecommendation: createSimpleFallback(selectedTeachers),\n provider: aiResponse.provider\n };\n }\n}\n\n// 🔧 UPDATED: createSimpleFallback to ensure proper distribution\nfunction createSimpleFallback(selectedTeachers) {\n // Simple fallback: split based on compatibility scores and reviews\n const sorted = [...selectedTeachers].sort((a, b) => {\n const aScore = (a.compatibilityScore || 0) + (a.reviews?.averageRating || 0) * 10;\n const bScore = (b.compatibilityScore || 0) + (b.reviews?.averageRating || 0) * 10;\n return bScore - aScore;\n });\n \n const top3 = sorted.slice(0, 3);\n const next3 = sorted.slice(3, 6); // 🆕 NEW: Next 3 teachers\n \n return {\n // Top 3 teachers (positions 1-3)\n topThreeTeachers: top3.map((teacher, index) => ({\n teacherId: teacher.id,\n name: teacher.name,\n matchScore: Math.max(80, teacher.compatibilityScore || 80) + (3 - index) * 3,\n matchReasons: [\n `High compatibility score (${teacher.compatibilityScore}%)`,\n teacher.reviews?.totalReviews > 0 \n ? `Rated ${teacher.reviews.averageRating}/5 by ${teacher.reviews.totalReviews} students`\n : 'Experienced professional teacher',\n teacher.videoDemo ? 'Professional video demo available' : 'Comprehensive teaching profile'\n ]\n })),\n \n // 🆕 NEW: Next 3 teachers (positions 4-6)\n nextThreeTeachers: next3.map((teacher, index) => ({\n teacherId: teacher.id,\n name: teacher.name,\n matchScore: Math.max(60, (teacher.compatibilityScore || 60) - 5) + (3 - index) * 2,\n matchReasons: [\n 'Good teaching qualifications',\n teacher.reviews?.totalReviews > 0 \n ? `${teacher.reviews.totalReviews} student reviews`\n : 'Professional teaching experience',\n 'Available for lessons'\n ]\n })),\n \n // Analysis remains the same\n analysis: {\n studentLevel: \"Not determined\",\n primaryGoals: [\"General English improvement\"],\n keyPreferences: [\"Not specified\"],\n rankingSummary: \"Fallback ranking based on teacher profiles and ratings\"\n }\n };\n}\n\n// 🔧 ENHANCED MAIN FUNCTION WITH FIXED TEACHER DISTRIBUTION\nasync function storeMultipleResponses(req, res) {\n try {\n const { responses, forceRefresh = false } = req.body;\n console.log('req :',req.userId);\n const userId = req.userId;\n const language = req.query.language || 'en';\n \n // Validate input\n if (!Array.isArray(responses) || responses.length === 0) {\n return res.status(400).json({\n success: false,\n message: 'Responses must be a non-empty array',\n data: null\n });\n }\n \n const results = [];\n const errors = [];\n \n // Process and store responses (keeping existing logic)\n for (let i = 0; i < responses.length; i++) {\n const { questionId, responseText, selectedOptions } = responses[i];\n \n try {\n if (!questionId) {\n errors.push({ questionId, error: 'Question ID is required' });\n continue;\n }\n \n const question = await QuestionBank.findOne({\n where: { id: questionId, is_active: 1 }\n });\n \n if (!question) {\n errors.push({ questionId, error: 'Question not found or inactive' });\n continue;\n }\n \n if (!responseText && (!selectedOptions || selectedOptions.length === 0)) {\n errors.push({ questionId, error: 'Response is required' });\n continue;\n }\n \n const existingResponse = await UserQuestionResponse.findOne({\n where: { user_id: userId, question_id: questionId }\n });\n \n let userResponse;\n \n if (existingResponse) {\n userResponse = await existingResponse.update({\n response_text: responseText || null,\n selected_options: selectedOptions || null,\n question_type: question.type,\n updated_at: new Date()\n });\n } else {\n userResponse = await UserQuestionResponse.create({\n user_id: userId,\n question_id: questionId,\n response_text: responseText || null,\n selected_options: selectedOptions || null,\n question_type: question.type\n });\n }\n \n results.push({ questionId: userResponse.question_id });\n \n } catch (error) {\n console.error(`Error processing response ${i}:`, error);\n errors.push({ questionId, error: 'Failed to process response' });\n }\n }\n \n // If too many errors, return early\n if (errors.length >= responses.length) {\n return res.status(400).json({\n success: false,\n message: 'Failed to process responses',\n data: null,\n errors: errors\n });\n }\n \n // 🚀 NEW: Convert indices to actual option values for analysis\n console.log('🔧 Converting option indices to actual values...');\n const studentResponseData = await convertIndicesToOptionValues(responses, language);\n console.log('forceRefresh :',forceRefresh);\n // 🚀 NEW: CHECK CACHE FIRST (unless forceRefresh is true)\n if (!forceRefresh) {\n try {\n console.log('🔍 Checking for existing recommendation cache...');\n \n const cachedRecommendation = await UserTeacherRecommendation.findValidRecommendation(\n userId, \n studentResponseData\n );\n \n if (cachedRecommendation && cachedRecommendation.isValid()) {\n console.log('✅ Found valid cached recommendation, returning cached data');\n \n // FIXED: Properly parse cached data if it's a string\n let cachedData = cachedRecommendation.recommendation_data;\n \n if (typeof cachedData === 'string') {\n try {\n cachedData = JSON.parse(cachedData);\n } catch (parseError) {\n console.error('Error parsing cached data:', parseError);\n // Continue with fresh generation if cache is corrupted\n }\n }\n \n // Convert timestamps for response\n const cacheInfo = {\n generatedAt: new Date(cachedRecommendation.created_at * 1000).toISOString(),\n expiresAt: cachedRecommendation.expires_at ? new Date(cachedRecommendation.expires_at * 1000).toISOString() : null,\n aiProvider: cachedRecommendation.ai_provider,\n aiStatus: cachedRecommendation.ai_status,\n totalTeachers: cachedRecommendation.total_teachers_count\n };\n \n return res.status(200).json({\n success: true,\n message: 'Cached teacher recommendations retrieved successfully',\n data: cachedData,\n cached: true,\n cacheInfo: cacheInfo\n });\n }\n \n console.log('❌ No valid cache found, generating new recommendations...');\n \n } catch (cacheError) {\n console.error('⚠️ Cache check failed, proceeding with generation:', cacheError);\n }\n } else {\n console.log('🔄 Force refresh requested, skipping cache...');\n }\n \n // Get ALL active teachers with comprehensive data (keeping your existing logic)\n let allTeachers = [];\n try {\n const basicTeachers = await Users.findAll({\n where: {\n role_name: 'teacher',\n status: 'active',\n ban: false,\n deleted_at: null\n },\n attributes: [\n 'id', 'full_name', 'email', 'bio', 'subject',\n 'education', 'experience', 'about', 'language', 'meeting_type',\n 'video_demo', 'video_demo_thumb', 'video_demo_source', \n 'headline', 'timezone', 'avatar'\n ],\n order: [['full_name', 'ASC']]\n });\n\n if (basicTeachers.length === 0) {\n return res.status(200).json({\n success: false,\n message: 'No active teachers available',\n data: null\n });\n }\n\n const teacherIds = basicTeachers.map(teacher => teacher.id);\n\n // Get ALL occupation data for teachers\n const teacherOccupations = await UserOccupation.findAll({\n where: {\n user_id: { [Op.in]: teacherIds },\n type: { [Op.in]: ['teachings', 'also_speaking', 'specialties', 'levels'] }\n },\n attributes: ['user_id', 'type', 'category_id', 'value'],\n order: [['user_id', 'ASC'], ['type', 'ASC']]\n });\n\n // Get teacher reviews\n const teacherReviews = await UserReview.findAll({\n where: {\n instructor_id: { [Op.in]: teacherIds }\n },\n attributes: ['instructor_id', 'rates', 'description']\n });\n\n // Organize data\n const occupationsByTeacher = {};\n teacherOccupations.forEach(occ => {\n if (!occupationsByTeacher[occ.user_id]) {\n occupationsByTeacher[occ.user_id] = {\n teachings: [],\n also_speaking: [],\n specialties: [],\n levels: []\n };\n }\n occupationsByTeacher[occ.user_id][occ.type].push(occ.value);\n });\n\n // Combine all teacher data\n allTeachers = basicTeachers.map(teacher => {\n const occupations = occupationsByTeacher[teacher.id] || {\n teachings: [],\n also_speaking: [],\n specialties: [],\n levels: []\n };\n\n const reviews = teacherReviews.filter(review => review.instructor_id === teacher.id);\n \n let averageRating = 0;\n let totalReviews = reviews.length;\n \n if (totalReviews > 0) {\n const ratesArray = reviews.map(review => parseFloat(review.rates));\n const totalRates = ratesArray.reduce((acc, rate) => acc + rate, 0);\n averageRating = (totalRates / totalReviews).toFixed(1);\n }\n\n return {\n id: teacher.id,\n name: teacher.full_name,\n email: teacher.email,\n avatar: teacher.avatar,\n bio: teacher.bio,\n subject: teacher.subject,\n education: teacher.education,\n experience: teacher.experience,\n about: teacher.about,\n language: teacher.language,\n meetingType: teacher.meeting_type,\n headline: teacher.headline,\n timezone: teacher.timezone,\n videoDemo: teacher.video_demo,\n videoDemoThumb: teacher.video_demo_thumb,\n videoDemoSource: teacher.video_demo_source,\n teachings: occupations.teachings,\n speakingLanguages: occupations.also_speaking,\n specialties: occupations.specialties,\n levels: occupations.levels,\n reviews: {\n totalReviews: totalReviews,\n averageRating: parseFloat(averageRating),\n recentReviews: reviews.slice(0, 3).map(review => ({\n overallRating: parseFloat(review.rates),\n description: review.description\n }))\n }\n };\n });\n\n console.log(`✅ Fetched ${allTeachers.length} total teachers`);\n\n } catch (error) {\n console.error('❌ Error fetching teacher data:', error);\n return res.status(500).json({\n success: false,\n message: 'Unable to fetch teacher information',\n data: null\n });\n }\n \n // 🔍 DEBUG: Track teacher flow\n console.log('🔍 TEACHER FLOW DEBUG:');\n console.log('1. Total teachers found:', allTeachers.length);\n console.log(' Teacher IDs:', allTeachers.map(t => t.id).join(', '));\n \n // SMART FILTERING: Get best teachers for AI processing\n const filterResult = filterBestTeachers(allTeachers, studentResponseData, 6);\n const { selectedTeachers, remainingTeachers, studentPreferences } = filterResult;\n \n console.log('2. After smart filtering:');\n console.log(' Selected for AI (top 6):', selectedTeachers.length);\n console.log(' Selected IDs:', selectedTeachers.map(t => t.id).join(', '));\n console.log(' Remaining teachers:', remainingTeachers.length);\n console.log(' Remaining IDs:', remainingTeachers.map(t => t.id).join(', '));\n \n if (selectedTeachers.length === 0) {\n return res.status(200).json({\n success: false,\n message: 'No suitable teachers found',\n data: null\n });\n }\n \n // AI FINAL RANKING: Get top 3 + next 3 from the selected teachers\n let aiRecommendation = null;\n let aiStatus = 'success';\n let aiMessage = 'Teacher recommendations generated successfully';\n let aiProvider = null;\n \n try {\n console.log('🤖 Starting AI final ranking for selected teachers...');\n \n const aiResult = await getFinalTeacherRanking(studentResponseData, selectedTeachers);\n \n if (aiResult.success) {\n aiRecommendation = aiResult.data;\n aiProvider = aiResult.provider;\n console.log(`✅ AI final ranking completed using ${aiResult.provider}`);\n } else {\n aiRecommendation = aiResult.fallbackRecommendation;\n aiStatus = 'fallback';\n aiMessage = 'Using fallback recommendations (AI temporarily unavailable)';\n aiProvider = 'Fallback';\n console.log('⚠️ Using fallback recommendation');\n }\n } catch (error) {\n console.error('❌ AI processing error:', error);\n aiRecommendation = createSimpleFallback(selectedTeachers);\n aiStatus = 'error';\n aiMessage = 'AI processing failed, using basic recommendations';\n aiProvider = 'Fallback';\n }\n \n if (!aiRecommendation) {\n return res.status(200).json({\n success: false,\n message: aiMessage,\n data: null\n });\n }\n\n console.log('3. After AI processing:');\n console.log(' AI returned topThreeTeachers:', aiRecommendation.topThreeTeachers?.length || 0);\n console.log(' AI returned nextThreeTeachers:', aiRecommendation.nextThreeTeachers?.length || 0);\n\n if (aiRecommendation.topThreeTeachers) {\n console.log(' Top 3 IDs:', aiRecommendation.topThreeTeachers.map(t => t.teacherId).join(', '));\n }\n if (aiRecommendation.nextThreeTeachers) {\n console.log(' Next 3 IDs:', aiRecommendation.nextThreeTeachers.map(t => t.teacherId).join(', '));\n }\n\n // Helper function to get full teacher data by ID\n const getFullTeacherData = (teacherId) => {\n const teacher = allTeachers.find(t => t.id === teacherId);\n if (!teacher) return null;\n \n return {\n teacherId: teacher.id,\n name: teacher.name,\n about: teacher.about,\n avatar: teacher.avatar,\n language: teacher.language,\n headline: teacher.headline,\n timezone: teacher.timezone,\n videoDemo: teacher.videoDemo,\n videoDemoThumb: teacher.videoDemoThumb,\n videoDemoSource: teacher.videoDemoSource,\n bio: teacher.bio,\n subject: teacher.subject,\n education: teacher.education,\n experience: teacher.experience,\n meetingType: teacher.meetingType,\n teachings: teacher.teachings,\n speakingLanguages: teacher.speakingLanguages,\n specialties: teacher.specialties,\n levels: teacher.levels,\n reviews: teacher.reviews,\n compatibilityScore: teacher.compatibilityScore || null\n };\n };\n \n // 🔧 BUILD FINAL RESPONSE WITH GUARANTEED ALL TEACHERS\n const responseData = {\n // Top 3 teachers (AI ranked)\n topThreeTeachers: aiRecommendation.topThreeTeachers?.map(teacher => {\n const fullTeacherData = getFullTeacherData(teacher.teacherId);\n return {\n ...fullTeacherData,\n matchScore: teacher.matchScore,\n matchReasons: teacher.matchReasons || []\n };\n }).filter(Boolean) || [],\n \n // Next 3 teachers (AI ranked or fallback)\n nextThreeTeachers: aiRecommendation.nextThreeTeachers?.map(teacher => {\n const fullTeacherData = getFullTeacherData(teacher.teacherId);\n return {\n ...fullTeacherData,\n matchScore: teacher.matchScore,\n matchReasons: teacher.matchReasons || []\n };\n }).filter(Boolean) || [],\n \n // 🆕 ALL REMAINING TEACHERS (ensures all teachers are included)\n otherMatchedTeachers: []\n };\n \n // Get teacher IDs that are already included in the response\n const topThreeIds = responseData.topThreeTeachers.map(t => t.teacherId);\n const nextThreeIds = responseData.nextThreeTeachers.map(t => t.teacherId);\n const includedIds = [...topThreeIds, ...nextThreeIds];\n \n // Find any teachers from the selected teachers that are missing\n const missingFromSelected = selectedTeachers.filter(teacher => \n !includedIds.includes(teacher.id)\n );\n \n // Combine missing teachers with remaining teachers for otherMatchedTeachers\n const allOtherTeachers = [...missingFromSelected, ...remainingTeachers];\n \n // Add all other teachers to the response\n responseData.otherMatchedTeachers = allOtherTeachers.map(teacher => ({\n teacherId: teacher.id,\n name: teacher.name,\n about: teacher.about,\n avatar: teacher.avatar,\n language: teacher.language,\n headline: teacher.headline,\n timezone: teacher.timezone,\n videoDemo: teacher.videoDemo,\n videoDemoThumb: teacher.videoDemoThumb,\n videoDemoSource: teacher.videoDemoSource,\n bio: teacher.bio,\n subject: teacher.subject,\n education: teacher.education,\n experience: teacher.experience,\n meetingType: teacher.meetingType,\n teachings: teacher.teachings,\n speakingLanguages: teacher.speakingLanguages,\n specialties: teacher.specialties,\n levels: teacher.levels,\n reviews: teacher.reviews,\n compatibilityScore: teacher.compatibilityScore || null,\n matchScore: 0,\n matchReasons: []\n }));\n \n // 📊 Calculate final summary\n const summary = {\n topThreeCount: responseData.topThreeTeachers.length,\n nextThreeCount: responseData.nextThreeTeachers.length,\n otherMatchedCount: responseData.otherMatchedTeachers.length,\n totalTeachersProcessed: allTeachers.length,\n totalTeachersReturned: responseData.topThreeTeachers.length + \n responseData.nextThreeTeachers.length + \n responseData.otherMatchedTeachers.length,\n aiProvider: aiProvider,\n aiStatus: aiStatus\n };\n \n console.log('4. Final response breakdown:');\n console.log(' topThreeTeachers:', summary.topThreeCount);\n console.log(' nextThreeTeachers:', summary.nextThreeCount);\n console.log(' otherMatchedTeachers:', summary.otherMatchedCount);\n console.log(' TOTAL RETURNED:', summary.totalTeachersReturned, '/', summary.totalTeachersProcessed);\n \n // Ensure we're returning all teachers\n if (summary.totalTeachersReturned !== summary.totalTeachersProcessed) {\n console.error(`⚠️ MISMATCH: Expected ${summary.totalTeachersProcessed} teachers, but returning ${summary.totalTeachersReturned}`);\n } else {\n console.log('✅ SUCCESS: All teachers included in response');\n }\n \n // 💾 NEW: SAVE TO CACHE\n try {\n console.log('💾 Saving recommendation to cache...');\n \n await UserTeacherRecommendation.createRecommendation(\n userId,\n studentResponseData,\n responseData,\n {\n totalTeachers: allTeachers.length,\n selectedTeachers: selectedTeachers.length,\n provider: aiProvider,\n status: aiStatus\n }\n );\n \n console.log('✅ Recommendation cached successfully');\n \n } catch (cacheError) {\n console.error('⚠️ Failed to cache recommendation:', cacheError);\n // Don't fail the request if caching fails\n }\n \n // API RESPONSE AS REQUESTED\n return res.status(200).json({\n success: true,\n message: aiMessage,\n data: responseData,\n cached: false,\n generated: true,\n summary: summary\n });\n \n } catch (err) {\n console.error('❌ Critical error in storeMultipleResponses:', err);\n return res.status(500).json({\n success: false,\n message: 'Internal server error occurred',\n data: null\n });\n }\n}\n\n// FIXED: Get cached recommendations only (with proper JSON parsing)\nasync function getCachedRecommendations(req, res) {\n try {\n const userId = req.userId;\n \n const cachedRecommendation = await UserTeacherRecommendation.findOne({\n where: {\n user_id: userId,\n is_active: true,\n deleted_at: null\n },\n order: [['created_at', 'DESC']]\n });\n \n if (!cachedRecommendation || !cachedRecommendation.isValid()) {\n return res.status(404).json({\n success: false,\n message: 'No valid cached recommendations found',\n data: null\n });\n }\n \n // FIXED: Properly parse the recommendation_data if it's a string\n let recommendationData = cachedRecommendation.recommendation_data;\n \n // If the data is still a string, parse it\n if (typeof recommendationData === 'string') {\n try {\n recommendationData = JSON.parse(recommendationData);\n } catch (parseError) {\n console.error('Error parsing cached recommendation data:', parseError);\n return res.status(500).json({\n success: false,\n message: 'Cached data format error',\n data: null\n });\n }\n }\n \n // Convert timestamps for response\n const cacheInfo = {\n generatedAt: new Date(cachedRecommendation.created_at * 1000).toISOString(),\n expiresAt: cachedRecommendation.expires_at ? new Date(cachedRecommendation.expires_at * 1000).toISOString() : null,\n aiProvider: cachedRecommendation.ai_provider,\n aiStatus: cachedRecommendation.ai_status,\n totalTeachers: cachedRecommendation.total_teachers_count\n };\n \n return res.status(200).json({\n success: true,\n message: 'Cached recommendations retrieved successfully',\n data: recommendationData, // NOW RETURNS PROPER JSON OBJECT\n cached: true,\n cacheInfo: cacheInfo\n });\n \n } catch (err) {\n console.error('Error fetching cached recommendations:', err);\n return res.status(500).json({\n success: false,\n message: 'Internal server error while fetching cached recommendations',\n data: null\n });\n }\n}\n\n// NEW: Clear user's recommendation cache\nasync function clearRecommendationCache(req, res) {\n try {\n const userId = req.userId;\n \n const affectedRows = await UserTeacherRecommendation.softDeleteUserRecommendations(userId);\n \n return res.status(200).json({\n success: true,\n message: `Cleared ${affectedRows} cached recommendations`,\n data: { clearedCount: affectedRows }\n });\n \n } catch (err) {\n console.error('Error clearing recommendation cache:', err);\n return res.status(500).json({\n success: false,\n message: 'Internal server error while clearing cache',\n data: null\n });\n }\n}\n\n// Get user's responses to questions\nasync function getUserResponses(req, res) {\n try {\n const userId = req.userId;\n const { questionId } = req.query;\n \n const whereCondition = { user_id: userId };\n \n // If specific question ID is provided\n if (questionId) {\n whereCondition.question_id = questionId;\n }\n \n const responses = await UserQuestionResponse.findAll({\n where: whereCondition,\n include: [\n {\n model: QuestionBank,\n as: 'question',\n attributes: ['id', 'question', 'type', 'options', 'question_order']\n }\n ],\n order: [['created_at', 'DESC']]\n });\n \n if (!responses || responses.length === 0) {\n return res.status(404).json({\n status: 'error',\n message: 'No responses found'\n });\n }\n \n // Format responses for mobile\n const formattedResponses = responses.map(response => ({\n id: response.id,\n questionId: response.question_id,\n responseText: response.response_text,\n selectedOptions: response.selected_options,\n questionType: response.question_type,\n createdAt: response.created_at,\n updatedAt: response.updated_at,\n question: response.question ? {\n id: response.question.id,\n text: response.question.question,\n type: response.question.type,\n order: response.question.question_order\n } : null\n }));\n \n return res.status(200).json({\n status: 'success',\n message: 'User responses retrieved successfully',\n count: formattedResponses.length,\n data: formattedResponses\n });\n \n } catch (err) {\n console.error('Error fetching user responses:', err);\n return res.status(500).json({\n status: 'error',\n message: 'Internal server error while fetching responses'\n });\n }\n}\n\n// Helper function to validate response based on question type\nfunction validateResponse(questionType, responseText, selectedOptions) {\n switch (questionType) {\n case 'text':\n if (!responseText || responseText.trim() === '') {\n return { isValid: false, message: 'Response text is required for text questions' };\n }\n return { isValid: true };\n \n case 'single-choice':\n case 'yes-no':\n if (!selectedOptions || !Array.isArray(selectedOptions) || selectedOptions.length !== 1) {\n return { isValid: false, message: `Exactly one option must be selected for ${questionType} questions` };\n }\n return { isValid: true };\n \n case 'multiple-choice':\n case 'checkbox':\n if (!selectedOptions || !Array.isArray(selectedOptions) || selectedOptions.length === 0) {\n return { isValid: false, message: `At least one option must be selected for ${questionType} questions` };\n }\n return { isValid: true };\n \n default:\n return { isValid: false, message: 'Invalid question type' };\n }\n}\n\n// MODULE EXPORTS - COMPLETE LIST INCLUDING NEW FUNCTION\nmodule.exports = {\n getAllActiveQuestions,\n getQuestionsByType,\n storeQuestionResponse,\n storeMultipleResponses, // Enhanced with caching and fixed teacher distribution\n getUserResponses,\n getCachedRecommendations,\n clearRecommendationCache,\n getUserQuestionnaireStatus // 🆕 NEW: Your main function for checking status\n};"}}}} |