Spaces:
Sleeping
Sleeping
| const express = require('express'); | |
| const cors = require('cors'); | |
| require('dotenv').config(); | |
| const { requireAuth, getTenantDb } = require('./services/auth'); | |
| const dbAdmin = require('./db'); | |
| // --- 🛡️ Global Crash Protection (10-Year Uptime Shield) --- | |
| process.on('uncaughtException', (err) => { | |
| console.error('CRITICAL [Uncaught Exception]:', err); | |
| }); | |
| process.on('unhandledRejection', (reason, promise) => { | |
| console.error('CRITICAL [Unhandled Rejection]:', reason); | |
| }); | |
| // ------------------------------------------------------------- | |
| const app = express(); | |
| app.use(cors()); | |
| app.use(express.json()); | |
| // Health check endpoint with environmental diagnostics | |
| app.get('/', (req, res) => { | |
| const dbModule = require('./db'); | |
| res.status(200).json({ | |
| status: 'Healthy', | |
| timestamp: new Date().toISOString(), | |
| nodeVersion: process.version, | |
| port: process.env.PORT || 7860, | |
| diagnostics: { | |
| SUPABASE_URL_EXISTS: !!process.env.SUPABASE_URL, | |
| SUPABASE_KEY_EXISTS: !!process.env.SUPABASE_KEY, | |
| SUPABASE_DB_URL_EXISTS: !!process.env.SUPABASE_DB_URL, | |
| SPREADSHEET_ID_EXISTS: !!process.env.SPREADSHEET_ID, | |
| GOOGLE_CREDS_JSON_EXISTS: !!process.env.GOOGLE_CREDS_JSON, | |
| NVIDIA_API_KEY_EXISTS: !!process.env.NVIDIA_API_KEY | |
| }, | |
| databaseInitializationError: dbModule.dbError || null | |
| }); | |
| }); | |
| // --- Modular Routing Blocks --- | |
| const leadRouter = require('./services/leadService'); | |
| const admissionRouter = require('./services/admissionService'); | |
| const aiRouter = require('./services/aiService'); | |
| const counselorRouter = require('./services/counselorService'); | |
| const publicRouter = require('./services/publicService'); | |
| const whatsappRouter = require('./services/whatsappService'); | |
| const courseRouter = require('./services/courseService'); | |
| const { router: notificationRouter, initNotificationService } = require('./services/notificationService'); | |
| // Initialize decoupled event subscribers for database auditing and real-time triggers | |
| initNotificationService(); | |
| // Legacy compatibility route rewrite for /api/update-lead and /server-api/update-lead | |
| app.put(['/api/update-lead', '/server-api/update-lead'], (req, res, next) => { | |
| req.url = '/update-lead'; | |
| leadRouter(req, res, next); | |
| }); | |
| // Mount Scoped API Sub-routers on both /api and /server-api | |
| ['/api', '/server-api'].forEach(prefix => { | |
| app.use(`${prefix}/leads`, leadRouter); | |
| app.use(`${prefix}/admissions`, admissionRouter); | |
| app.use(`${prefix}/ai-insights`, aiRouter); | |
| app.use(`${prefix}/insights`, aiRouter); | |
| app.use(`${prefix}/notifications`, notificationRouter); | |
| app.use(`${prefix}/counselors`, counselorRouter); | |
| app.use(`${prefix}/public`, publicRouter); | |
| app.use(`${prefix}/whatsapp`, whatsappRouter); | |
| app.use(`${prefix}/courses`, courseRouter); | |
| }); | |
| // GET /api/stats and /server-api/stats - Dashboard KPI metrics scoped strictly by active organization | |
| app.get(['/api/stats', '/server-api/stats'], requireAuth, async (req, res) => { | |
| try { | |
| const db = getTenantDb(req); | |
| // Fetch active leads, admissions, counselors, and activities in parallel | |
| const [leadsRes, admissionsRes, counselorsRes, activitiesRes] = await Promise.all([ | |
| db.from('active_leads').select('id, status, followup_time, course_interested, counselor_id'), | |
| db.from('active_admissions').select('id, fees, course, lead_id'), | |
| db.from('active_counselors').select('id, name'), | |
| db.from('lead_activities').select('activity_type, description') | |
| ]); | |
| if (leadsRes.error) throw leadsRes.error; | |
| if (admissionsRes.error) throw admissionsRes.error; | |
| if (counselorsRes.error) throw counselorsRes.error; | |
| const leads = leadsRes.data || []; | |
| const admissions = admissionsRes.data || []; | |
| const counselors = counselorsRes.data || []; | |
| const activities = (activitiesRes && !activitiesRes.error) ? activitiesRes.data : []; | |
| const totalLeads = leads.length; | |
| const activeLeads = leads.filter(l => !['Not Interested', 'Converted', 'Lost'].includes(l.status)).length; | |
| const lostLeads = leads.filter(l => ['Not Interested', 'Lost'].includes(l.status)).length; | |
| const admissionsCount = admissions.length; | |
| const revenue = admissions.reduce((sum, adm) => sum + (parseFloat(adm.fees) || 0), 0); | |
| // Follow-ups due today/immediate or pending | |
| const followupsDue = leads.filter(l => l.status === 'Pending' || l.followup_time === 'Today' || l.followup_time === 'Immediate').length; | |
| // Trending courses list | |
| const courseCounts = {}; | |
| leads.forEach(l => { | |
| if (l.course_interested) { | |
| courseCounts[l.course_interested] = (courseCounts[l.course_interested] || 0) + 1; | |
| } | |
| }); | |
| const trendingCourses = Object.entries(courseCounts) | |
| .map(([name, count]) => ({ name, count })) | |
| .sort((a, b) => b.count - a.count) | |
| .slice(0, 5); | |
| // Counselor stats | |
| const counselorStats = counselors.map(c => { | |
| const cLeads = leads.filter(l => l.counselor_id === c.id); | |
| const cAdmissions = admissions.filter(a => cLeads.some(l => l.id === a.lead_id)); | |
| const conversionRate = cLeads.length > 0 ? Math.round((cAdmissions.length / cLeads.length) * 100) : 0; | |
| return { | |
| name: c.name, | |
| leadsAssigned: cLeads.length, | |
| conversions: cAdmissions.length, | |
| conversionRate | |
| }; | |
| }); | |
| // WhatsApp Stats | |
| const whatsappTotal = activities.filter(a => a.activity_type === 'WhatsApp Follow-up Sent').length; | |
| const whatsappFollowups = activities.filter(a => a.description && a.description.includes('Follow-up Reminder')).length; | |
| const whatsappAdmissions = activities.filter(a => a.description && a.description.includes('Admission Reminder')).length; | |
| res.json({ | |
| totalLeads, | |
| activeLeads, | |
| lostLeads, | |
| admissions: admissionsCount, | |
| revenue, | |
| followupsDue, | |
| trendingCourses, | |
| counselorStats, | |
| whatsappStats: { | |
| totalSent: whatsappTotal, | |
| followupReminders: whatsappFollowups, | |
| admissionReminders: whatsappAdmissions | |
| } | |
| }); | |
| } catch (error) { | |
| console.error('Error fetching KPI metrics:', error); | |
| res.status(500).json({ error: 'Internal Server Error' }); | |
| } | |
| }); | |
| // POST /api/sync-sheet and /server-api/sync-sheet - Keep sync endpoint strictly for manual importing if requested | |
| app.post(['/api/sync-sheet', '/server-api/sync-sheet'], requireAuth, async (req, res) => { | |
| try { | |
| const { syncSheetsToDB } = require('./services/googleSheets'); | |
| await syncSheetsToDB(null); | |
| res.json({ message: 'Sheets sync completed successfully' }); | |
| } catch (error) { | |
| console.error('Sheets sync failed:', error); | |
| res.status(500).json({ error: 'Sheets sync failed' }); | |
| } | |
| }); | |
| // --- 🛡️ Global Express Error Handler --- | |
| app.use((err, req, res, next) => { | |
| console.error('Express Pipeline Error:', err); | |
| res.status(500).json({ error: 'Internal Server Error (Caught by Global Handler)' }); | |
| }); | |
| // ----------------------------------------- | |
| const PORT = process.env.PORT || 7860; | |
| app.listen(PORT, '0.0.0.0', () => { | |
| console.log(`Production API Gateway running on port ${PORT}`); | |
| }); | |