Spaces:
Sleeping
Sleeping
File size: 7,308 Bytes
de09409 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | 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}`);
});
|