// server3.js - Premium Services API import express from 'express'; import cors from 'cors'; import helmet from 'helmet'; import dotenv from 'dotenv'; import { open } from 'sqlite'; import sqlite3 from 'sqlite3'; import jwt from 'jsonwebtoken'; import { v4 as uuidv4 } from 'uuid'; import path from 'path'; import { fileURLToPath } from 'url'; import { simulateFinancialProjection } from '../src/utils/FinancialProjectionService.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); dotenv.config({ path: path.resolve(__dirname, '..', '.env') }); const app = express(); const PORT = process.env.PREMIUM_PORT || 5002; let db; const initDB = async () => { try { db = await open({ filename: '/home/jcoakley/aptiva-dev1-app/user_profile.db', driver: sqlite3.Database }); console.log('Connected to user_profile.db for Premium Services.'); } catch (error) { console.error('Error connecting to premium database:', error); } }; initDB(); app.use(helmet()); app.use(express.json()); const allowedOrigins = ['https://dev1.aptivaai.com']; app.use(cors({ origin: allowedOrigins, credentials: true })); const authenticatePremiumUser = (req, res, next) => { const token = req.headers.authorization?.split(' ')[1]; if (!token) return res.status(401).json({ error: 'Premium authorization required' }); try { const SECRET_KEY = process.env.SECRET_KEY || 'supersecurekey'; const { userId } = jwt.verify(token, SECRET_KEY); req.userId = userId; next(); } catch (error) { return res.status(403).json({ error: 'Invalid or expired token' }); } }; // Get latest selected planned path app.get('/api/premium/planned-path/latest', authenticatePremiumUser, async (req, res) => { try { const row = await db.get( `SELECT * FROM career_path WHERE user_id = ? ORDER BY start_date DESC LIMIT 1`, [req.userId] ); res.json(row || {}); } catch (error) { console.error('Error fetching latest career path:', error); res.status(500).json({ error: 'Failed to fetch latest planned path' }); } }); // Get all planned paths for the user app.get('/api/premium/planned-path/all', authenticatePremiumUser, async (req, res) => { try { const rows = await db.all( `SELECT * FROM career_path WHERE user_id = ? ORDER BY start_date ASC`, [req.userId] ); res.json({ careerPath: rows }); } catch (error) { console.error('Error fetching career paths:', error); res.status(500).json({ error: 'Failed to fetch planned paths' }); } }); // Save a new planned path app.post('/api/premium/planned-path', authenticatePremiumUser, async (req, res) => { let { career_name } = req.body; if (!career_name) { return res.status(400).json({ error: 'Career name is required.' }); } try { // Ensure that career_name is always a string if (typeof career_name !== 'string') { console.warn('career_name was not a string. Converting to string.'); career_name = String(career_name); // Convert to string } // Check if the career path already exists for the user const existingCareerPath = await db.get( `SELECT id FROM career_path WHERE user_id = ? AND career_name = ?`, [req.userId, career_name] ); if (existingCareerPath) { return res.status(200).json({ message: 'Career path already exists. Would you like to reload it or create a new one?', career_path_id: existingCareerPath.id, action_required: 'reload_or_create' }); } // Define a new career path id and insert into the database const newCareerPathId = uuidv4(); await db.run( `INSERT INTO career_path (id, user_id, career_name) VALUES (?, ?, ?)`, [newCareerPathId, req.userId, career_name] ); res.status(201).json({ message: 'Career path saved.', career_path_id: newCareerPathId, action_required: 'new_created' }); } catch (error) { console.error('Error saving career path:', error); res.status(500).json({ error: 'Failed to save career path.' }); } }); // Milestones premium services // Save a new milestone app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => { const rawMilestones = Array.isArray(req.body.milestones) ? req.body.milestones : [req.body]; const errors = []; const validMilestones = []; for (const [index, m] of rawMilestones.entries()) { const { milestone_type, title, description, date, career_path_id, salary_increase, status = 'planned', date_completed = null, context_snapshot = null, progress = 0, } = m; // Validate required fields if (!milestone_type || !title || !description || !date || !career_path_id) { errors.push({ index, error: 'Missing required fields', title, // <-- Add the title for identification date, details: { milestone_type: !milestone_type ? 'Required' : undefined, title: !title ? 'Required' : undefined, description: !description ? 'Required' : undefined, date: !date ? 'Required' : undefined, career_path_id: !career_path_id ? 'Required' : undefined, } }); continue; } validMilestones.push({ id: uuidv4(), // ✅ assign UUID for unique milestone ID user_id: req.userId, milestone_type, title, description, date, career_path_id, salary_increase: salary_increase || null, status, date_completed, context_snapshot, progress }); } if (errors.length) { console.warn('❗ Some milestones failed validation. Logging malformed records...'); console.warn(JSON.stringify(errors, null, 2)); return res.status(400).json({ error: 'Some milestones are invalid', errors }); } try { const insertPromises = validMilestones.map(m => db.run( `INSERT INTO milestones ( id, user_id, milestone_type, title, description, date, career_path_id, salary_increase, status, date_completed, context_snapshot, progress, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`, [ m.id, m.user_id, m.milestone_type, m.title, m.description, m.date, m.career_path_id, m.salary_increase, m.status, m.date_completed, m.context_snapshot, m.progress ] ) ); await Promise.all(insertPromises); res.status(201).json({ message: 'Milestones saved successfully', count: validMilestones.length }); } catch (error) { console.error('Error saving milestones:', error); res.status(500).json({ error: 'Failed to save milestones' }); } }); // Get all milestones app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => { try { const { careerPathId } = req.query; if (!careerPathId) { return res.status(400).json({ error: 'careerPathId is required' }); } const milestones = await db.all( `SELECT * FROM milestones WHERE user_id = ? AND career_path_id = ? ORDER BY date ASC`, [req.userId, careerPathId] ); res.json({ milestones }); } catch (error) { console.error('Error fetching milestones:', error); res.status(500).json({ error: 'Failed to fetch milestones' }); } }); // Update an existing milestone app.put('/api/premium/milestones/:id', authenticatePremiumUser, async (req, res) => { try { const { id } = req.params; const numericId = parseInt(id, 10); // 👈 Block-defined for SQLite safety if (isNaN(numericId)) { return res.status(400).json({ error: 'Invalid milestone ID' }); } const { milestone_type, title, description, date, progress, status, date_completed, salary_increase, context_snapshot, } = req.body; // Explicit required field validation if (!milestone_type || !title || !description || !date || progress === undefined) { return res.status(400).json({ error: 'Missing required fields', details: { milestone_type: !milestone_type ? 'Required' : undefined, title: !title ? 'Required' : undefined, description: !description ? 'Required' : undefined, date: !date ? 'Required' : undefined, progress: progress === undefined ? 'Required' : undefined, } }); } console.log('Updating milestone with:', { milestone_type, title, description, date, progress, status, date_completed, salary_increase, context_snapshot, id: numericId, userId: req.userId }); await db.run( `UPDATE milestones SET milestone_type = ?, title = ?, description = ?, date = ?, progress = ?, status = ?, date_completed = ?, salary_increase = ?, context_snapshot = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND user_id = ?`, [ milestone_type, title, description, date, progress || 0, status || 'planned', date_completed, salary_increase || null, context_snapshot || null, numericId, // 👈 used here in the query req.userId ] ); res.status(200).json({ message: 'Milestone updated successfully' }); } catch (error) { console.error('Error updating milestone:', error.message, error.stack); res.status(500).json({ error: 'Failed to update milestone' }); } }); app.delete('/api/premium/milestones/:id', authenticatePremiumUser, async (req, res) => { const { id } = req.params; try { await db.run(`DELETE FROM milestones WHERE id = ? AND user_id = ?`, [id, req.userId]); res.status(200).json({ message: 'Milestone deleted successfully' }); } catch (error) { console.error('Error deleting milestone:', error); res.status(500).json({ error: 'Failed to delete milestone' }); } }); //Financial Profile premium services //Get financial profile app.get('/api/premium/financial-profile', authenticatePremiumUser, async (req, res) => { try { const row = await db.get(`SELECT * FROM financial_profile WHERE user_id = ?`, [req.userId]); res.json(row || {}); } catch (error) { console.error('Error fetching financial profile:', error); res.status(500).json({ error: 'Failed to fetch financial profile' }); } }); // Backend code (server3.js) // Save or update financial profile app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req, res) => { const { currentSalary, additionalIncome, monthlyExpenses, monthlyDebtPayments, retirementSavings, retirementContribution, emergencyFund, inCollege, expectedGraduation, partTimeIncome, tuitionPaid, collegeLoanTotal, selectedSchool, selectedProgram, programType, isFullyOnline, creditHoursPerYear, hoursCompleted, careerPathId, loanDeferralUntilGraduation, tuition, programLength, interestRate, loanTerm, extraPayment, expectedSalary } = req.body; try { // **Call the simulateFinancialProjection function here** with all the incoming data const { projectionData, loanPaidOffMonth } = simulateFinancialProjection({ currentSalary: req.body.currentSalary + (req.body.additionalIncome || 0), monthlyExpenses: req.body.monthlyExpenses, monthlyDebtPayments: req.body.monthlyDebtPayments || 0, studentLoanAmount: req.body.collegeLoanTotal, // ✅ UPDATED to dynamic fields from frontend interestRate: req.body.interestRate, loanTerm: req.body.loanTerm, extraPayment: req.body.extraPayment || 0, expectedSalary: req.body.expectedSalary, emergencySavings: req.body.emergencyFund, retirementSavings: req.body.retirementSavings, monthlyRetirementContribution: req.body.retirementContribution, monthlyEmergencyContribution: 0, gradDate: req.body.expectedGraduation, fullTimeCollegeStudent: req.body.inCollege, partTimeIncome: req.body.partTimeIncome, startDate: new Date(), programType: req.body.programType, isFullyOnline: req.body.isFullyOnline, creditHoursPerYear: req.body.creditHoursPerYear, calculatedTuition: req.body.tuition, manualTuition: 0, hoursCompleted: req.body.hoursCompleted, loanDeferralUntilGraduation: req.body.loanDeferralUntilGraduation, programLength: req.body.programLength }); // Now you can save the profile or update the database with the new data const existing = await db.get(`SELECT id FROM financial_profile WHERE user_id = ?`, [req.userId]); if (existing) { // Updating existing profile await db.run(` UPDATE financial_profile SET current_salary = ?, additional_income = ?, monthly_expenses = ?, monthly_debt_payments = ?, retirement_savings = ?, retirement_contribution = ?, emergency_fund = ?, in_college = ?, expected_graduation = ?, part_time_income = ?, tuition_paid = ?, college_loan_total = ?, selected_school = ?, selected_program = ?, program_type = ?, is_online = ?, credit_hours_per_year = ?, hours_completed = ?, tuition = ?, loan_deferral_until_graduation = ?, program_length = ?, interest_rate = ?, loan_term = ?, extra_payment = ?, expected_salary = ?, updated_at = CURRENT_TIMESTAMP WHERE user_id = ?`, [ currentSalary, additionalIncome, monthlyExpenses, monthlyDebtPayments, retirementSavings, retirementContribution, emergencyFund, inCollege ? 1 : 0, expectedGraduation, partTimeIncome, tuitionPaid, collegeLoanTotal, selectedSchool, selectedProgram, programType, isFullyOnline, creditHoursPerYear, hoursCompleted, tuition, loanDeferralUntilGraduation, programLength, interestRate, loanTerm, extraPayment, expectedSalary, // ✅ added new fields req.userId ] ); } else { // Insert a new profile await db.run(` INSERT INTO financial_profile ( id, user_id, current_salary, additional_income, monthly_expenses, monthly_debt_payments, retirement_savings, retirement_contribution, emergency_fund, in_college, expected_graduation, part_time_income, tuition_paid, college_loan_total, selected_school, selected_program, program_type, is_online, credit_hours_per_year, calculated_tuition, loan_deferral_until_graduation, hours_completed, tuition, program_length, interest_rate, loan_term, extra_payment, expected_salary ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ uuidv4(), req.userId, currentSalary, additionalIncome, monthlyExpenses, monthlyDebtPayments, retirementSavings, retirementContribution, emergencyFund, inCollege ? 1 : 0, expectedGraduation, partTimeIncome, tuitionPaid, collegeLoanTotal, selectedSchool, selectedProgram, programType, isFullyOnline, creditHoursPerYear, hoursCompleted, tuition, loanDeferralUntilGraduation, programLength, interestRate, loanTerm, extraPayment, expectedSalary // ✅ added new fields ] ); } // Return the financial simulation results (calculated projection data) to the frontend res.status(200).json({ message: 'Financial profile saved.', projectionData, loanPaidOffMonth, emergencyFund: emergencyFund // explicitly add the emergency fund here }); console.log("Request body:", req.body); } catch (error) { console.error('Error saving financial profile:', error); res.status(500).json({ error: 'Failed to save financial profile.' }); } }); //PreimumOnboarding //Career onboarding app.post('/api/premium/onboarding/career', authenticatePremiumUser, async (req, res) => { const { career_name, status, start_date, projected_end_date } = req.body; try { const careerPathId = uuidv4(); await db.run(` INSERT INTO career_path (id, user_id, career_name, status, start_date, projected_end_date) VALUES (?, ?, ?, ?, ?, ?)`, [careerPathId, req.userId, career_name, status || 'planned', start_date || new Date().toISOString(), projected_end_date || null] ); res.status(201).json({ message: 'Career onboarding data saved.', careerPathId }); } catch (error) { console.error('Error saving career onboarding data:', error); res.status(500).json({ error: 'Failed to save career onboarding data.' }); } }); //Financial onboarding app.post('/api/premium/onboarding/financial', authenticatePremiumUser, async (req, res) => { const { current_salary, additional_income, monthly_expenses, monthly_debt_payments, retirement_savings, retirement_contribution, emergency_fund } = req.body; try { await db.run(` INSERT INTO financial_profile ( id, user_id, current_salary, additional_income, monthly_expenses, monthly_debt_payments, retirement_savings, retirement_contribution, emergency_fund, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`, [ uuidv4(), req.userId, current_salary, additional_income, monthly_expenses, monthly_debt_payments, retirement_savings, retirement_contribution, emergency_fund ] ); res.status(201).json({ message: 'Financial onboarding data saved.' }); } catch (error) { console.error('Error saving financial onboarding data:', error); res.status(500).json({ error: 'Failed to save financial onboarding data.' }); } }); //College onboarding app.post('/api/premium/onboarding/college', authenticatePremiumUser, async (req, res) => { const { in_college, expected_graduation, selected_school, selected_program, program_type, is_online, credit_hours_per_year, hours_completed } = req.body; try { await db.run(` INSERT INTO financial_profile ( id, user_id, in_college, expected_graduation, selected_school, selected_program, program_type, is_online, credit_hours_per_year, hours_completed, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`, [ uuidv4(), req.userId, in_college ? 1 : 0, expected_graduation, selected_school, selected_program, program_type, is_online, credit_hours_per_year, hours_completed ] ); res.status(201).json({ message: 'College onboarding data saved.' }); } catch (error) { console.error('Error saving college onboarding data:', error); res.status(500).json({ error: 'Failed to save college onboarding data.' }); } }); //Financial Projection Premium Services // Save financial projection for a specific careerPathId app.post('/api/premium/financial-projection/:careerPathId', authenticatePremiumUser, async (req, res) => { const { careerPathId } = req.params; const { projectionData } = req.body; // JSON containing detailed financial projections try { const projectionId = uuidv4(); await db.run(` INSERT INTO financial_projections (id, user_id, career_path_id, projection_json) VALUES (?, ?, ?, ?)`, [projectionId, req.userId, careerPathId, JSON.stringify(projectionData)] ); res.status(201).json({ message: 'Financial projection saved.', projectionId }); } catch (error) { console.error('Error saving financial projection:', error); res.status(500).json({ error: 'Failed to save financial projection.' }); } }); // Get financial projection for a specific careerPathId app.get('/api/premium/financial-projection/:careerPathId', authenticatePremiumUser, async (req, res) => { const { careerPathId } = req.params; try { const projection = await db.get(` SELECT projection_json FROM financial_projections WHERE user_id = ? AND career_path_id = ?`, [req.userId, careerPathId] ); if (!projection) { return res.status(404).json({ error: 'Projection not found.' }); } res.status(200).json(JSON.parse(projection.projection_json)); } catch (error) { console.error('Error fetching financial projection:', error); res.status(500).json({ error: 'Failed to fetch financial projection.' }); } }); // ROI Analysis (placeholder logic) app.get('/api/premium/roi-analysis', authenticatePremiumUser, async (req, res) => { try { const userCareer = await db.get( `SELECT * FROM career_path WHERE user_id = ? ORDER BY start_date DESC LIMIT 1`, [req.userId] ); if (!userCareer) return res.status(404).json({ error: 'No planned path found for user' }); const roi = { jobTitle: userCareer.job_title, salary: userCareer.salary, tuition: 50000, netGain: userCareer.salary - 50000 }; res.json(roi); } catch (error) { console.error('Error calculating ROI:', error); res.status(500).json({ error: 'Failed to calculate ROI' }); } }); app.listen(PORT, () => { console.log(`Premium server running on http://localhost:${PORT}`); });