// server3.js 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'; // If you still need the projection logic somewhere else 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' }); } }; /* ------------------------------------------------------------------ CAREER PROFILE ENDPOINTS (Renamed from planned-path to career-profile) ------------------------------------------------------------------ */ // GET the latest selected career profile app.get('/api/premium/career-profile/latest', authenticatePremiumUser, async (req, res) => { try { const row = await db.get(` SELECT * FROM career_paths WHERE user_id = ? ORDER BY start_date DESC LIMIT 1 `, [req.userId]); res.json(row || {}); } catch (error) { console.error('Error fetching latest career profile:', error); res.status(500).json({ error: 'Failed to fetch latest career profile' }); } }); // GET all career profiles for the user app.get('/api/premium/career-profile/all', authenticatePremiumUser, async (req, res) => { try { const rows = await db.all(` SELECT * FROM career_paths WHERE user_id = ? ORDER BY start_date ASC `, [req.userId]); res.json({ careerPaths: rows }); } catch (error) { console.error('Error fetching career profiles:', error); res.status(500).json({ error: 'Failed to fetch career profiles' }); } }); // POST a new career profile app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res) => { const { career_name, status, start_date, projected_end_date, college_enrollment_status, currently_working } = req.body; // If you need to ensure the user gave us a career_name: if (!career_name) { return res.status(400).json({ error: 'career_name is required.' }); } try { const newCareerPathId = uuidv4(); const now = new Date().toISOString(); await db.run(` INSERT INTO career_paths ( id, user_id, career_name, status, start_date, projected_end_date, college_enrollment_status, currently_working, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(user_id, career_name) DO UPDATE SET status = excluded.status, start_date = excluded.start_date, projected_end_date = excluded.projected_end_date, college_enrollment_status = excluded.college_enrollment_status, currently_working = excluded.currently_working, updated_at = ? `, [ newCareerPathId, // id req.userId, // user_id career_name, // career_name status || 'planned', // status (if null, default to 'planned') start_date || now, projected_end_date || null, college_enrollment_status || null, currently_working || null, now, // created_at now, // updated_at on initial insert now // updated_at on conflict ]); // Optionally fetch the row's ID after upsert const result = await db.get(` SELECT id FROM career_paths WHERE user_id = ? AND career_name = ? `, [req.userId, career_name]); res.status(200).json({ message: 'Career profile upserted.', career_path_id: result?.id }); } catch (error) { console.error('Error upserting career profile:', error); res.status(500).json({ error: 'Failed to upsert career profile.' }); } }); app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => { try { const { milestone_type, title, description, date, career_path_id, progress, status, new_salary } = req.body; if (!milestone_type || !title || !date || !career_path_id) { return res.status(400).json({ error: 'Missing required fields', details: { milestone_type, title, date, career_path_id } }); } const id = uuidv4(); const now = new Date().toISOString(); await db.run(` INSERT INTO milestones ( id, user_id, career_path_id, milestone_type, title, description, date, progress, status, new_salary, -- store the full new salary if provided created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ id, req.userId, career_path_id, milestone_type, title, description || '', date, progress || 0, status || 'planned', new_salary || null, now, now ]); // Return the newly created milestone object // (No tasks initially, so tasks = []) const newMilestone = { id, user_id: req.userId, career_path_id, milestone_type, title, description: description || '', date, progress: progress || 0, status: status || 'planned', new_salary: new_salary || null, tasks: [] }; res.status(201).json(newMilestone); } catch (err) { console.error('Error creating milestone:', err); res.status(500).json({ error: 'Failed to create milestone.' }); } }); app.put('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async (req, res) => { try { const { milestoneId } = req.params; const { milestone_type, title, description, date, career_path_id, progress, status, new_salary } = req.body; // Check if milestone exists and belongs to user const existing = await db.get(` SELECT * FROM milestones WHERE id = ? AND user_id = ? `, [milestoneId, req.userId]); if (!existing) { return res.status(404).json({ error: 'Milestone not found or not yours.' }); } // Update const now = new Date().toISOString(); await db.run(` UPDATE milestones SET milestone_type = ?, title = ?, description = ?, date = ?, career_path_id = ?, progress = ?, status = ?, new_salary = ?, updated_at = ? WHERE id = ? `, [ milestone_type || existing.milestone_type, title || existing.title, description || existing.description, date || existing.date, career_path_id || existing.career_path_id, progress != null ? progress : existing.progress, status || existing.status, new_salary != null ? new_salary : existing.new_salary, now, milestoneId ]); // Return the updated record with tasks const updatedMilestoneRow = await db.get(` SELECT * FROM milestones WHERE id = ? `, [milestoneId]); // Fetch tasks for this milestone const tasks = await db.all(` SELECT * FROM tasks WHERE milestone_id = ? `, [milestoneId]); const updatedMilestone = { ...updatedMilestoneRow, tasks: tasks || [] }; res.json(updatedMilestone); } catch (err) { console.error('Error updating milestone:', err); res.status(500).json({ error: 'Failed to update milestone.' }); } }); app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => { const { careerPathId } = req.query; try { // 1. Fetch the milestones for this user + path const milestones = await db.all(` SELECT * FROM milestones WHERE user_id = ? AND career_path_id = ? `, [req.userId, careerPathId]); // 2. For each milestone, fetch tasks const milestoneIds = milestones.map(m => m.id); let tasksByMilestone = {}; if (milestoneIds.length > 0) { const tasks = await db.all(` SELECT * FROM tasks WHERE milestone_id IN (${milestoneIds.map(() => '?').join(',')}) `, milestoneIds); tasksByMilestone = tasks.reduce((acc, t) => { if (!acc[t.milestone_id]) acc[t.milestone_id] = []; acc[t.milestone_id].push(t); return acc; }, {}); } // 3. Attach tasks to each milestone object const milestonesWithTasks = milestones.map(m => ({ ...m, tasks: tasksByMilestone[m.id] || [] })); res.json({ milestones: milestonesWithTasks }); } catch (err) { console.error('Error fetching milestones with tasks:', err); res.status(500).json({ error: 'Failed to fetch milestones.' }); } }); /* ------------------------------------------------------------------ FINANCIAL PROFILES (Renamed emergency_contribution) ------------------------------------------------------------------ */ app.get('/api/premium/financial-profile', authenticatePremiumUser, async (req, res) => { try { const row = await db.get(` SELECT * FROM financial_profiles 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' }); } }); app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req, res) => { const { current_salary, additional_income, monthly_expenses, monthly_debt_payments, retirement_savings, retirement_contribution, emergency_fund, emergency_contribution, extra_cash_emergency_pct, extra_cash_retirement_pct } = req.body; try { // Check if row exists const existing = await db.get(` SELECT user_id FROM financial_profiles WHERE user_id = ? `, [req.userId]); if (!existing) { // Insert new row await db.run(` INSERT INTO financial_profiles ( user_id, current_salary, additional_income, monthly_expenses, monthly_debt_payments, retirement_savings, emergency_fund, retirement_contribution, emergency_contribution, extra_cash_emergency_pct, extra_cash_retirement_pct, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) `, [ req.userId, current_salary || 0, additional_income || 0, monthly_expenses || 0, monthly_debt_payments || 0, retirement_savings || 0, emergency_fund || 0, retirement_contribution || 0, emergency_contribution || 0, // store new field extra_cash_emergency_pct || 0, extra_cash_retirement_pct || 0 ]); } else { // Update existing await db.run(` UPDATE financial_profiles SET current_salary = ?, additional_income = ?, monthly_expenses = ?, monthly_debt_payments = ?, retirement_savings = ?, emergency_fund = ?, retirement_contribution = ?, emergency_contribution = ?, extra_cash_emergency_pct = ?, extra_cash_retirement_pct = ?, updated_at = CURRENT_TIMESTAMP WHERE user_id = ? `, [ current_salary || 0, additional_income || 0, monthly_expenses || 0, monthly_debt_payments || 0, retirement_savings || 0, emergency_fund || 0, retirement_contribution || 0, emergency_contribution || 0, // updated field extra_cash_emergency_pct || 0, extra_cash_retirement_pct || 0, req.userId ]); } res.json({ message: 'Financial profile saved/updated.' }); } catch (error) { console.error('Error saving financial profile:', error); res.status(500).json({ error: 'Failed to save financial profile.' }); } }); /* ------------------------------------------------------------------ COLLEGE PROFILES ------------------------------------------------------------------ */ app.post('/api/premium/college-profile', authenticatePremiumUser, async (req, res) => { const { career_path_id, selected_school, selected_program, program_type, is_in_state, is_in_district, college_enrollment_status, is_online, credit_hours_per_year, credit_hours_required, hours_completed, program_length, expected_graduation, existing_college_debt, interest_rate, loan_term, loan_deferral_until_graduation, extra_payment, expected_salary, academic_calendar, annual_financial_aid, tuition, tuition_paid } = req.body; try { const id = uuidv4(); const user_id = req.userId; await db.run(` INSERT INTO college_profiles ( id, user_id, career_path_id, selected_school, selected_program, program_type, is_in_state, is_in_district, college_enrollment_status, annual_financial_aid, is_online, credit_hours_per_year, hours_completed, program_length, credit_hours_required, expected_graduation, existing_college_debt, interest_rate, loan_term, loan_deferral_until_graduation, extra_payment, expected_salary, academic_calendar, tuition, tuition_paid, created_at, updated_at ) VALUES ( ?, -- id ?, -- user_id ?, -- career_path_id ?, -- selected_school ?, -- selected_program ?, -- program_type ?, -- is_in_state ?, -- is_in_district ?, -- college_enrollment_status ?, -- annual_financial_aid ?, -- is_online ?, -- credit_hours_per_year ?, -- hours_completed ?, -- program_length ?, -- credit_hours_required ?, -- expected_graduation ?, -- existing_college_debt ?, -- interest_rate ?, -- loan_term ?, -- loan_deferral_until_graduation ?, -- extra_payment ?, -- expected_salary ?, -- academic_calendar ?, -- tuition ?, -- tuition_paid CURRENT_TIMESTAMP, CURRENT_TIMESTAMP ) `, [ id, user_id, career_path_id, selected_school, selected_program, program_type || null, is_in_state ? 1 : 0, is_in_district ? 1 : 0, college_enrollment_status || null, annual_financial_aid || 0, is_online ? 1 : 0, credit_hours_per_year || 0, hours_completed || 0, program_length || 0, credit_hours_required || 0, expected_graduation || null, existing_college_debt || 0, interest_rate || 0, loan_term || 10, loan_deferral_until_graduation ? 1 : 0, extra_payment || 0, expected_salary || 0, academic_calendar || 'semester', tuition || 0, tuition_paid || 0 ]); res.status(201).json({ message: 'College profile saved.', collegeProfileId: id }); } catch (error) { console.error('Error saving college profile:', error); res.status(500).json({ error: 'Failed to save college profile.' }); } }); /* ------------------------------------------------------------------ FINANCIAL PROJECTIONS ------------------------------------------------------------------ */ app.post('/api/premium/financial-projection/:careerPathId', authenticatePremiumUser, async (req, res) => { const { careerPathId } = req.params; const { projectionData, loanPaidOffMonth, finalEmergencySavings, finalRetirementSavings, finalLoanBalance } = req.body; try { const projectionId = uuidv4(); await db.run(` INSERT INTO financial_projections ( id, user_id, career_path_id, projection_data, loan_paid_off_month, final_emergency_savings, final_retirement_savings, final_loan_balance, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) `, [ projectionId, req.userId, careerPathId, JSON.stringify(projectionData), loanPaidOffMonth || null, finalEmergencySavings || 0, finalRetirementSavings || 0, finalLoanBalance || 0 ]); 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.' }); } }); app.get('/api/premium/financial-projection/:careerPathId', authenticatePremiumUser, async (req, res) => { const { careerPathId } = req.params; try { const row = await db.get(` SELECT projection_data, loan_paid_off_month, final_emergency_savings, final_retirement_savings, final_loan_balance FROM financial_projections WHERE user_id = ? AND career_path_id = ? ORDER BY created_at DESC LIMIT 1 `, [req.userId, careerPathId]); if (!row) { return res.status(404).json({ error: 'Projection not found.' }); } const parsedProjectionData = JSON.parse(row.projection_data); res.status(200).json({ projectionData: parsedProjectionData, loanPaidOffMonth: row.loan_paid_off_month, finalEmergencySavings: row.final_emergency_savings, finalRetirementSavings: row.final_retirement_savings, finalLoanBalance: row.final_loan_balance }); } catch (error) { console.error('Error fetching financial projection:', error); res.status(500).json({ error: 'Failed to fetch financial projection.' }); } }); // POST create a new task app.post('/api/premium/tasks', authenticatePremiumUser, async (req, res) => { const { milestone_id, // which milestone this belongs to user_id, // might come from token or from body title, description, due_date } = req.body; // Insert into tasks table // Return the new task in JSON }); // GET tasks for a milestone app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => { const { careerPathId } = req.query; try { // 1. Fetch the milestones for this user + path const milestones = await db.all(` SELECT * FROM milestones WHERE user_id = ? AND career_path_id = ? `, [req.userId, careerPathId]); // 2. For each milestone, fetch tasks (or do a single join—see note below) // We'll do it in Node code for clarity: const milestoneIds = milestones.map(m => m.id); let tasksByMilestone = {}; if (milestoneIds.length > 0) { const tasks = await db.all(` SELECT * FROM tasks WHERE milestone_id IN (${milestoneIds.map(() => '?').join(',')}) `, milestoneIds); // Group tasks by milestone_id tasksByMilestone = tasks.reduce((acc, t) => { if (!acc[t.milestone_id]) acc[t.milestone_id] = []; acc[t.milestone_id].push(t); return acc; }, {}); } // 3. Attach tasks to each milestone object const milestonesWithTasks = milestones.map(m => ({ ...m, tasks: tasksByMilestone[m.id] || [] })); res.json({ milestones: milestonesWithTasks }); } catch (err) { console.error('Error fetching milestones with tasks:', err); res.status(500).json({ error: 'Failed to fetch milestones.' }); } }); app.listen(PORT, () => { console.log(`Premium server running on http://localhost:${PORT}`); });