// 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'; 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 ------------------------------------------------------------------ */ // 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' }); } }); // GET a single career profile (scenario) by ID app.get('/api/premium/career-profile/:careerPathId', authenticatePremiumUser, async (req, res) => { const { careerPathId } = req.params; try { const row = await db.get(` SELECT * FROM career_paths WHERE id = ? AND user_id = ? `, [careerPathId, req.userId]); if (!row) { return res.status(404).json({ error: 'Career path (scenario) not found or not yours.' }); } res.json(row); } catch (error) { console.error('Error fetching single career profile:', error); res.status(500).json({ error: 'Failed to fetch career profile by ID.' }); } }); // POST a new career profile (upsert) app.post('/api/premium/career-profile', authenticatePremiumUser, async (req, res) => { const { scenario_title, career_name, status, start_date, projected_end_date, college_enrollment_status, currently_working, planned_monthly_expenses, planned_monthly_debt_payments, planned_monthly_retirement_contribution, planned_monthly_emergency_contribution, planned_surplus_emergency_pct, planned_surplus_retirement_pct, planned_additional_income } = req.body; 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, scenario_title, career_name, status, start_date, projected_end_date, college_enrollment_status, currently_working, planned_monthly_expenses, planned_monthly_debt_payments, planned_monthly_retirement_contribution, planned_monthly_emergency_contribution, planned_surplus_emergency_pct, planned_surplus_retirement_pct, planned_additional_income, 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, planned_monthly_expenses = excluded.planned_monthly_expenses, planned_monthly_debt_payments = excluded.planned_monthly_debt_payments, planned_monthly_retirement_contribution = excluded.planned_monthly_retirement_contribution, planned_monthly_emergency_contribution = excluded.planned_monthly_emergency_contribution, planned_surplus_emergency_pct = excluded.planned_surplus_emergency_pct, planned_surplus_retirement_pct = excluded.planned_surplus_retirement_pct, planned_additional_income = excluded.planned_additional_income, updated_at = ? `, [ newCareerPathId, req.userId, scenario_title || null, career_name, status || 'planned', start_date || now, projected_end_date || null, college_enrollment_status || null, currently_working || null, planned_monthly_expenses ?? null, planned_monthly_debt_payments ?? null, planned_monthly_retirement_contribution ?? null, planned_monthly_emergency_contribution ?? null, planned_surplus_emergency_pct ?? null, planned_surplus_retirement_pct ?? null, planned_additional_income ?? null, now, now, now ]); 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.' }); } }); // DELETE a career path (scenario) by ID (and associated data) app.delete('/api/premium/career-profile/:careerPathId', authenticatePremiumUser, async (req, res) => { const { careerPathId } = req.params; try { // 1) Confirm that this career_path belongs to the user const existing = await db.get(` SELECT id FROM career_paths WHERE id = ? AND user_id = ? `, [careerPathId, req.userId]); if (!existing) { return res.status(404).json({ error: 'Career path not found or not yours.' }); } // 2) Delete the college_profile for this scenario await db.run(` DELETE FROM college_profiles WHERE user_id = ? AND career_path_id = ? `, [req.userId, careerPathId]); // 3) Delete scenario’s milestones (and tasks/impacts) const scenarioMilestones = await db.all(` SELECT id FROM milestones WHERE user_id = ? AND career_path_id = ? `, [req.userId, careerPathId]); const milestoneIds = scenarioMilestones.map((m) => m.id); if (milestoneIds.length > 0) { const placeholders = milestoneIds.map(() => '?').join(','); // Delete tasks await db.run(` DELETE FROM tasks WHERE milestone_id IN (${placeholders}) `, milestoneIds); // Delete impacts await db.run(` DELETE FROM milestone_impacts WHERE milestone_id IN (${placeholders}) `, milestoneIds); // Finally delete the milestones themselves await db.run(` DELETE FROM milestones WHERE id IN (${placeholders}) `, milestoneIds); } // 4) Delete the career_path row await db.run(` DELETE FROM career_paths WHERE user_id = ? AND id = ? `, [req.userId, careerPathId]); res.json({ message: 'Career path and related data successfully deleted.' }); } catch (error) { console.error('Error deleting career path:', error); res.status(500).json({ error: 'Failed to delete career path.' }); } }); /* ------------------------------------------------------------------ Milestone ENDPOINTS ------------------------------------------------------------------ */ // CREATE one or more milestones app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => { try { const body = req.body; // CASE 1: If client sent { milestones: [ ... ] }, do a bulk insert if (Array.isArray(body.milestones)) { const createdMilestones = []; for (const m of body.milestones) { const { milestone_type, title, description, date, career_path_id, progress, status, new_salary, is_universal } = m; if (!milestone_type || !title || !date || !career_path_id) { return res.status(400).json({ error: 'One or more milestones missing required fields', details: m }); } 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, is_universal, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ id, req.userId, career_path_id, milestone_type, title, description || '', date, progress || 0, status || 'planned', new_salary || null, is_universal ? 1 : 0, now, now ]); createdMilestones.push({ 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, is_universal: is_universal ? 1 : 0, tasks: [] }); } return res.status(201).json(createdMilestones); } // CASE 2: Single milestone creation const { milestone_type, title, description, date, career_path_id, progress, status, new_salary, is_universal } = 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, is_universal, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ id, req.userId, career_path_id, milestone_type, title, description || '', date, progress || 0, status || 'planned', new_salary || null, is_universal ? 1 : 0, now, now ]); 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, is_universal: is_universal ? 1 : 0, tasks: [] }; res.status(201).json(newMilestone); } catch (err) { console.error('Error creating milestone(s):', err); res.status(500).json({ error: 'Failed to create milestone(s).' }); } }); // UPDATE an existing 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, is_universal } = 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.' }); } const now = new Date().toISOString(); const finalMilestoneType = milestone_type || existing.milestone_type; const finalTitle = title || existing.title; const finalDesc = description || existing.description; const finalDate = date || existing.date; const finalCareerPath = career_path_id || existing.career_path_id; const finalProgress = progress != null ? progress : existing.progress; const finalStatus = status || existing.status; const finalSalary = new_salary != null ? new_salary : existing.new_salary; const finalIsUniversal = is_universal != null ? (is_universal ? 1 : 0) : existing.is_universal; await db.run(` UPDATE milestones SET milestone_type = ?, title = ?, description = ?, date = ?, career_path_id = ?, progress = ?, status = ?, new_salary = ?, is_universal = ?, updated_at = ? WHERE id = ? AND user_id = ? `, [ finalMilestoneType, finalTitle, finalDesc, finalDate, finalCareerPath, finalProgress, finalStatus, finalSalary, finalIsUniversal, now, milestoneId, req.userId ]); // Return the updated milestone with tasks const updatedMilestoneRow = await db.get(` SELECT * FROM milestones WHERE id = ? `, [milestoneId]); 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.' }); } }); // GET all milestones for a given careerPathId app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => { const { careerPathId } = req.query; try { // universal milestones if (careerPathId === 'universal') { const universalRows = await db.all(` SELECT * FROM milestones WHERE user_id = ? AND is_universal = 1 `, [req.userId]); const milestoneIds = universalRows.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; }, {}); } const uniMils = universalRows.map(m => ({ ...m, tasks: tasksByMilestone[m.id] || [] })); return res.json({ milestones: uniMils }); } // else fetch by careerPathId const milestones = await db.all(` SELECT * FROM milestones WHERE user_id = ? AND career_path_id = ? `, [req.userId, careerPathId]); 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; }, {}); } 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.' }); } }); // COPY an existing milestone to other scenarios app.post('/api/premium/milestone/copy', authenticatePremiumUser, async (req, res) => { try { const { milestoneId, scenarioIds } = req.body; if (!milestoneId || !Array.isArray(scenarioIds) || scenarioIds.length === 0) { return res.status(400).json({ error: 'Missing milestoneId or scenarioIds.' }); } const original = await db.get(` SELECT * FROM milestones WHERE id = ? AND user_id = ? `, [milestoneId, req.userId]); if (!original) { return res.status(404).json({ error: 'Milestone not found or not owned by user.' }); } if (original.is_universal !== 1) { await db.run(` UPDATE milestones SET is_universal = 1 WHERE id = ? AND user_id = ? `, [ milestoneId, req.userId ]); original.is_universal = 1; } let originId = original.origin_milestone_id || original.id; if (!original.origin_milestone_id) { await db.run(` UPDATE milestones SET origin_milestone_id = ? WHERE id = ? AND user_id = ? `, [ originId, milestoneId, req.userId ]); } const tasks = await db.all(` SELECT * FROM tasks WHERE milestone_id = ? `, [milestoneId]); const impacts = await db.all(` SELECT * FROM milestone_impacts WHERE milestone_id = ? `, [milestoneId]); const now = new Date().toISOString(); const copiesCreated = []; for (let scenarioId of scenarioIds) { if (scenarioId === original.career_path_id) continue; // skip if same scenario const newMilestoneId = uuidv4(); const isUniversal = 1; await db.run(` INSERT INTO milestones ( id, user_id, career_path_id, milestone_type, title, description, date, progress, status, new_salary, is_universal, origin_milestone_id, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ newMilestoneId, req.userId, scenarioId, original.milestone_type, original.title, original.description, original.date, original.progress, original.status, original.new_salary, isUniversal, originId, now, now ]); // copy tasks for (let t of tasks) { const newTaskId = uuidv4(); await db.run(` INSERT INTO tasks ( id, milestone_id, user_id, title, description, due_date, status, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, 'not_started', ?, ?) `, [ newTaskId, newMilestoneId, req.userId, t.title, t.description, t.due_date || null, now, now ]); } // copy impacts for (let imp of impacts) { const newImpactId = uuidv4(); await db.run(` INSERT INTO milestone_impacts ( id, milestone_id, impact_type, direction, amount, start_date, end_date, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ newImpactId, newMilestoneId, imp.impact_type, imp.direction, imp.amount, imp.start_date || null, imp.end_date || null, now, now ]); } copiesCreated.push(newMilestoneId); } return res.json({ originalId: milestoneId, origin_milestone_id: originId, copiesCreated }); } catch (err) { console.error('Error copying milestone:', err); res.status(500).json({ error: 'Failed to copy milestone.' }); } }); // DELETE milestone from ALL scenarios app.delete('/api/premium/milestones/:milestoneId/all', authenticatePremiumUser, async (req, res) => { const { milestoneId } = req.params; try { // 1) Fetch the milestone const existing = await db.get(` SELECT id, user_id, origin_milestone_id FROM milestones WHERE id = ? AND user_id = ? `, [milestoneId, req.userId]); if (!existing) { return res.status(404).json({ error: 'Milestone not found or not owned by user.' }); } // We'll remove all milestones (the original + copies) referencing the same originId const originId = existing.origin_milestone_id || existing.id; // Find all those milestone IDs const allMilsToDelete = await db.all(` SELECT id FROM milestones WHERE user_id = ? AND (id = ? OR origin_milestone_id = ?) `, [req.userId, originId, originId]); const milIDs = allMilsToDelete.map(m => m.id); if (milIDs.length > 0) { const placeholders = milIDs.map(() => '?').join(','); // Delete tasks for those milestones await db.run(` DELETE FROM tasks WHERE milestone_id IN (${placeholders}) `, milIDs); // Delete impacts for those milestones await db.run(` DELETE FROM milestone_impacts WHERE milestone_id IN (${placeholders}) `, milIDs); // Finally remove the milestones themselves await db.run(` DELETE FROM milestones WHERE user_id = ? AND (id = ? OR origin_milestone_id = ?) `, [req.userId, originId, originId]); } res.json({ message: 'Deleted from all scenarios' }); } catch (err) { console.error('Error deleting milestone from all scenarios:', err); res.status(500).json({ error: 'Failed to delete milestone from all scenarios.' }); } }); // DELETE milestone from THIS scenario only app.delete('/api/premium/milestones/:milestoneId', authenticatePremiumUser, async (req, res) => { const { milestoneId } = req.params; try { // 1) check user ownership const existing = await db.get(` SELECT id, user_id FROM milestones WHERE id = ? AND user_id = ? `, [milestoneId, req.userId]); if (!existing) { return res.status(404).json({ error: 'Milestone not found or not owned by user.' }); } // 2) Delete tasks associated with this milestone await db.run(` DELETE FROM tasks WHERE milestone_id = ? `, [milestoneId]); // 3) Delete milestone impacts await db.run(` DELETE FROM milestone_impacts WHERE milestone_id = ? `, [milestoneId]); // 4) Finally remove the milestone await db.run(` DELETE FROM milestones WHERE id = ? AND user_id = ? `, [milestoneId, req.userId]); res.json({ message: 'Milestone deleted from this scenario.' }); } catch (err) { console.error('Error deleting single milestone:', err); res.status(500).json({ error: 'Failed to delete milestone.' }); } }); /* ------------------------------------------------------------------ FINANCIAL PROFILES ------------------------------------------------------------------ */ 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 { const existing = await db.get(` SELECT user_id FROM financial_profiles WHERE user_id = ? `, [req.userId]); if (!existing) { 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, extra_cash_emergency_pct || 0, extra_cash_retirement_pct || 0 ]); } else { 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, 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 user_id = req.userId; const newId = uuidv4(); 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 ) ON CONFLICT(user_id, career_path_id, selected_school, selected_program, program_type) DO UPDATE SET is_in_state = excluded.is_in_state, is_in_district = excluded.is_in_district, college_enrollment_status = excluded.college_enrollment_status, annual_financial_aid = excluded.annual_financial_aid, is_online = excluded.is_online, credit_hours_per_year = excluded.credit_hours_per_year, hours_completed = excluded.hours_completed, program_length = excluded.program_length, credit_hours_required = excluded.credit_hours_required, expected_graduation = excluded.expected_graduation, existing_college_debt = excluded.existing_college_debt, interest_rate = excluded.interest_rate, loan_term = excluded.loan_term, loan_deferral_until_graduation = excluded.loan_deferral_until_graduation, extra_payment = excluded.extra_payment, expected_salary = excluded.expected_salary, academic_calendar = excluded.academic_calendar, tuition = excluded.tuition, tuition_paid = excluded.tuition_paid, updated_at = CURRENT_TIMESTAMP `, { ':id': newId, ':user_id': user_id, ':career_path_id': career_path_id, ':selected_school': selected_school, ':selected_program': selected_program, ':program_type': program_type || null, ':is_in_state': is_in_state ? 1 : 0, ':is_in_district': is_in_district ? 1 : 0, ':college_enrollment_status': college_enrollment_status || null, ':annual_financial_aid': annual_financial_aid || 0, ':is_online': is_online ? 1 : 0, ':credit_hours_per_year': credit_hours_per_year || 0, ':hours_completed': hours_completed || 0, ':program_length': program_length || 0, ':credit_hours_required': credit_hours_required || 0, ':expected_graduation': expected_graduation || null, ':existing_college_debt': existing_college_debt || 0, ':interest_rate': interest_rate || 0, ':loan_term': loan_term || 10, ':loan_deferral_until_graduation': loan_deferral_until_graduation ? 1 : 0, ':extra_payment': extra_payment || 0, ':expected_salary': expected_salary || 0, ':academic_calendar': academic_calendar || 'semester', ':tuition': tuition || 0, ':tuition_paid': tuition_paid || 0 }); res.status(201).json({ message: 'College profile upsert done.' }); } catch (error) { console.error('Error saving college profile:', error); res.status(500).json({ error: 'Failed to save college profile.' }); } }); app.get('/api/premium/college-profile', authenticatePremiumUser, async (req, res) => { const { careerPathId } = req.query; try { const row = await db.get(` SELECT * FROM college_profiles WHERE user_id = ? AND career_path_id = ? ORDER BY created_at DESC LIMIT 1 `, [req.userId, careerPathId]); res.json(row || {}); } catch (error) { console.error('Error fetching college profile:', error); res.status(500).json({ error: 'Failed to fetch 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.' }); } }); /* ------------------------------------------------------------------ TASK ENDPOINTS ------------------------------------------------------------------ */ // CREATE a new task (already existed, repeated here for clarity) app.post('/api/premium/tasks', authenticatePremiumUser, async (req, res) => { try { const { milestone_id, title, description, due_date } = req.body; if (!milestone_id || !title) { return res.status(400).json({ error: 'Missing required fields', details: { milestone_id, title } }); } // Confirm milestone is owned by this user const milestone = await db.get(` SELECT user_id FROM milestones WHERE id = ? `, [milestone_id]); if (!milestone || milestone.user_id !== req.userId) { return res.status(403).json({ error: 'Milestone not found or not yours.' }); } const taskId = uuidv4(); const now = new Date().toISOString(); await db.run(` INSERT INTO tasks ( id, milestone_id, user_id, title, description, due_date, status, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, 'not_started', ?, ?) `, [ taskId, milestone_id, req.userId, title, description || '', due_date || null, now, now ]); const newTask = { id: taskId, milestone_id, user_id: req.userId, title, description: description || '', due_date: due_date || null, status: 'not_started' }; res.status(201).json(newTask); } catch (err) { console.error('Error creating task:', err); res.status(500).json({ error: 'Failed to create task.' }); } }); // UPDATE an existing task app.put('/api/premium/tasks/:taskId', authenticatePremiumUser, async (req, res) => { try { const { taskId } = req.params; const { title, description, due_date, status } = req.body; // Check ownership const existing = await db.get(` SELECT user_id FROM tasks WHERE id = ? `, [taskId]); if (!existing || existing.user_id !== req.userId) { return res.status(404).json({ error: 'Task not found or not owned by you.' }); } const now = new Date().toISOString(); await db.run(` UPDATE tasks SET title = COALESCE(?, title), description = COALESCE(?, description), due_date = COALESCE(?, due_date), status = COALESCE(?, status), updated_at = ? WHERE id = ? `, [ title || null, description || null, due_date || null, status || null, now, taskId ]); // Return the updated task const updatedTask = await db.get(` SELECT * FROM tasks WHERE id = ? `, [taskId]); res.json(updatedTask); } catch (err) { console.error('Error updating task:', err); res.status(500).json({ error: 'Failed to update task.' }); } }); // DELETE a task app.delete('/api/premium/tasks/:taskId', authenticatePremiumUser, async (req, res) => { try { const { taskId } = req.params; // Verify ownership const existing = await db.get(` SELECT user_id FROM tasks WHERE id = ? `, [taskId]); if (!existing || existing.user_id !== req.userId) { return res.status(404).json({ error: 'Task not found or not owned by you.' }); } await db.run(` DELETE FROM tasks WHERE id = ? `, [taskId]); res.json({ message: 'Task deleted successfully.' }); } catch (err) { console.error('Error deleting task:', err); res.status(500).json({ error: 'Failed to delete task.' }); } }); /* ------------------------------------------------------------------ MILESTONE IMPACTS ENDPOINTS ------------------------------------------------------------------ */ app.get('/api/premium/milestone-impacts', authenticatePremiumUser, async (req, res) => { try { const { milestone_id } = req.query; if (!milestone_id) { return res.status(400).json({ error: 'milestone_id is required.' }); } // Verify the milestone belongs to this user const milestoneRow = await db.get(` SELECT user_id FROM milestones WHERE id = ? `, [milestone_id]); if (!milestoneRow || milestoneRow.user_id !== req.userId) { return res.status(404).json({ error: 'Milestone not found or not owned by this user.' }); } const impacts = await db.all(` SELECT id, milestone_id, impact_type, direction, amount, start_date, end_date, created_at, updated_at FROM milestone_impacts WHERE milestone_id = ? ORDER BY created_at ASC `, [milestone_id]); res.json({ impacts }); } catch (err) { console.error('Error fetching milestone impacts:', err); res.status(500).json({ error: 'Failed to fetch milestone impacts.' }); } }); app.post('/api/premium/milestone-impacts', authenticatePremiumUser, async (req, res) => { try { const { milestone_id, impact_type, direction = 'subtract', amount = 0, start_date = null, end_date = null, created_at, updated_at } = req.body; if (!milestone_id || !impact_type) { return res.status(400).json({ error: 'milestone_id and impact_type are required.' }); } // Confirm user owns the milestone const milestoneRow = await db.get(` SELECT user_id FROM milestones WHERE id = ? `, [milestone_id]); if (!milestoneRow || milestoneRow.user_id !== req.userId) { return res.status(403).json({ error: 'Milestone not found or not owned by this user.' }); } const newUUID = uuidv4(); const now = new Date().toISOString(); const finalCreated = created_at || now; const finalUpdated = updated_at || now; await db.run(` INSERT INTO milestone_impacts ( id, milestone_id, impact_type, direction, amount, start_date, end_date, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ newUUID, milestone_id, impact_type, direction, amount, start_date, end_date, finalCreated, finalUpdated ]); const insertedRow = await db.get(` SELECT id, milestone_id, impact_type, direction, amount, start_date, end_date, created_at, updated_at FROM milestone_impacts WHERE id = ? `, [newUUID]); return res.status(201).json(insertedRow); } catch (err) { console.error('Error creating milestone impact:', err); return res.status(500).json({ error: 'Failed to create milestone impact.' }); } }); // UPDATE an existing milestone impact app.put('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser, async (req, res) => { try { const { impactId } = req.params; const { milestone_id, impact_type, direction = 'subtract', amount = 0, start_date = null, end_date = null } = req.body; // check ownership const existing = await db.get(` SELECT mi.id, m.user_id FROM milestone_impacts mi JOIN milestones m ON mi.milestone_id = m.id WHERE mi.id = ? `, [impactId]); if (!existing || existing.user_id !== req.userId) { return res.status(404).json({ error: 'Impact not found or not owned by user.' }); } const now = new Date().toISOString(); await db.run(` UPDATE milestone_impacts SET milestone_id = ?, impact_type = ?, direction = ?, amount = ?, start_date = ?, end_date = ?, updated_at = ? WHERE id = ? `, [ milestone_id, impact_type, direction, amount, start_date, end_date, now, impactId ]); const updatedRow = await db.get(` SELECT id, milestone_id, impact_type, direction, amount, start_date, end_date, created_at, updated_at FROM milestone_impacts WHERE id = ? `, [impactId]); res.json(updatedRow); } catch (err) { console.error('Error updating milestone impact:', err); res.status(500).json({ error: 'Failed to update milestone impact.' }); } }); // DELETE an existing milestone impact app.delete('/api/premium/milestone-impacts/:impactId', authenticatePremiumUser, async (req, res) => { try { const { impactId } = req.params; // check ownership const existing = await db.get(` SELECT mi.id, m.user_id FROM milestone_impacts mi JOIN milestones m ON mi.milestone_id = m.id WHERE mi.id = ? `, [impactId]); if (!existing || existing.user_id !== req.userId) { return res.status(404).json({ error: 'Impact not found or not owned by user.' }); } await db.run(` DELETE FROM milestone_impacts WHERE id = ? `, [impactId]); res.json({ message: 'Impact deleted successfully.' }); } catch (err) { console.error('Error deleting milestone impact:', err); res.status(500).json({ error: 'Failed to delete milestone impact.' }); } }); /* ------------------------------------------------------------------ FALLBACK (404 for unmatched routes) ------------------------------------------------------------------ */ app.use((req, res) => { console.warn(`No route matched for ${req.method} ${req.originalUrl}`); res.status(404).json({ error: 'Not found' }); }); app.listen(PORT, () => { console.log(`Premium server running on http://localhost:${PORT}`); });