acadflow / server.js
skandaedutech's picture
Upload 14 files
0d3ef02 verified
Raw
History Blame
7.31 kB
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}`);
});