// 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 fs from 'fs/promises'; import multer from 'multer'; import mammoth from 'mammoth'; import { fileURLToPath } from 'url'; import pkg from 'pdfjs-dist'; import OpenAI from 'openai'; // --- Basic file init --- const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const rootPath = path.resolve(__dirname, '..'); // Up one level const env = process.env.NODE_ENV?.trim() || 'development'; const envPath = path.resolve(rootPath, `.env.${env}`); dotenv.config({ path: envPath }); // Load .env file const app = express(); const PORT = process.env.PREMIUM_PORT || 5002; const { getDocument } = pkg; 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.' }); } }); /* ------------------------------------------------------------------ RESUME OPTIMIZATION ENDPOINT ------------------------------------------------------------------ */ const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); // Setup file upload via multer const upload = multer({ dest: 'uploads/' }); // Basic usage gating config const MAX_MONTHLY_REWRITES_PREMIUM = 2; // Helper: build GPT prompt const buildResumePrompt = (resumeText, jobTitle, jobDescription) => ` You are an expert resume writer specialized in precisely tailoring existing resumes for optimal ATS compatibility and explicit alignment with provided job descriptions. STRICT GUIDELINES: 1. DO NOT invent any new job titles, employers, dates, locations, compensation details, or roles not explicitly stated in the user's original resume. 2. Creatively but realistically reframe, reposition, and explicitly recontextualize the user's existing professional experiences and skills to clearly demonstrate alignment with the provided job description. 3. Emphasize transferable skills, tasks, and responsibilities from the user's provided resume content that directly match the requirements and responsibilities listed in the job description. 4. Clearly and explicitly incorporate exact keywords, responsibilities, skills, and competencies directly from the provided job description. 5. Minimize or entirely remove irrelevant technical jargon or specific software names not directly aligned with the job description. 6. Avoid generic résumé clichés (e.g., "results-driven," "experienced professional," "dedicated leader," "dynamic professional," etc.). 7. NEVER directly reuse specific details such as salary information, compensation, or other company-specific information from the provided job description. Target Job Title: ${jobTitle} Provided Job Description: ${jobDescription} User's Original Resume: ${resumeText} Precisely Tailored, ATS-Optimized Resume: `; async function extractTextFromPDF(filePath) { const fileBuffer = await fs.readFile(filePath); const uint8Array = new Uint8Array(fileBuffer); // Convert Buffer explicitly const pdfDoc = await getDocument({ data: uint8Array }).promise; let text = ''; for (let pageNum = 1; pageNum <= pdfDoc.numPages; pageNum++) { const page = await pdfDoc.getPage(pageNum); const pageText = await page.getTextContent(); text += pageText.items.map(item => item.str).join(' '); } return text; } // Your corrected endpoint with limits correctly returned: app.post( '/api/premium/resume/optimize', upload.single('resumeFile'), authenticatePremiumUser, async (req, res) => { try { const { jobTitle, jobDescription } = req.body; if (!jobTitle || !jobDescription || !req.file) { return res.status(400).json({ error: 'Missing required fields.' }); } const userId = req.userId; const now = new Date(); const userProfile = await db.get( `SELECT is_premium, is_pro_premium, resume_optimizations_used, resume_limit_reset, resume_booster_count FROM user_profile WHERE user_id = ?`, [userId] ); let userPlan = 'basic'; if (userProfile?.is_pro_premium) userPlan = 'pro'; else if (userProfile?.is_premium) userPlan = 'premium'; const weeklyLimits = { basic: 1, premium: 2, pro: 5 }; const userWeeklyLimit = weeklyLimits[userPlan] || 0; let resetDate = new Date(userProfile.resume_limit_reset); if (!userProfile.resume_limit_reset || now > resetDate) { resetDate = new Date(now); resetDate.setDate(now.getDate() + 7); await db.run( `UPDATE user_profile SET resume_optimizations_used = 0, resume_limit_reset = ? WHERE user_id = ?`, [resetDate.toISOString(), userId] ); userProfile.resume_optimizations_used = 0; } const totalLimit = userWeeklyLimit + (userProfile.resume_booster_count || 0); if (userProfile.resume_optimizations_used >= totalLimit) { return res.status(403).json({ error: 'Weekly resume optimization limit reached. Consider purchasing a booster pack.' }); } const filePath = req.file.path; const mimeType = req.file.mimetype; let resumeText = ''; if (mimeType === 'application/pdf') { resumeText = await extractTextFromPDF(filePath); } else if ( mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || mimeType === 'application/msword' ) { const result = await mammoth.extractRawText({ path: filePath }); resumeText = result.value; } else { await fs.unlink(filePath); return res.status(400).json({ error: 'Unsupported or corrupted file upload.' }); } const prompt = buildResumePrompt(resumeText, jobTitle, jobDescription); const completion = await openai.chat.completions.create({ model: 'gpt-4-turbo', messages: [{ role: 'user', content: prompt }], temperature: 0.7, }); const optimizedResume = completion?.choices?.[0]?.message?.content?.trim() || ''; await db.run( `UPDATE user_profile SET resume_optimizations_used = resume_optimizations_used + 1 WHERE user_id = ?`, [userId] ); const remainingOptimizations = totalLimit - (userProfile.resume_optimizations_used + 1); await fs.unlink(filePath); res.json({ optimizedResume, remainingOptimizations, resetDate: resetDate.toISOString() // <-- explicitly returned here! }); } catch (err) { console.error('Error optimizing resume:', err); res.status(500).json({ error: 'Failed to optimize resume.' }); } } ); app.get( '/api/premium/resume/remaining', authenticatePremiumUser, async (req, res) => { try { const userId = req.userId; const now = new Date(); const userProfile = await db.get( `SELECT is_premium, is_pro_premium, resume_optimizations_used, resume_limit_reset, resume_booster_count FROM user_profile WHERE user_id = ?`, [userId] ); let userPlan = 'basic'; if (userProfile?.is_pro_premium) userPlan = 'pro'; else if (userProfile?.is_premium) userPlan = 'premium'; const weeklyLimits = { basic: 1, premium: 2, pro: 5 }; const userWeeklyLimit = weeklyLimits[userPlan] || 0; let resetDate = new Date(userProfile.resume_limit_reset); if (!userProfile.resume_limit_reset || now > resetDate) { resetDate = new Date(now); resetDate.setDate(now.getDate() + 7); await db.run( `UPDATE user_profile SET resume_optimizations_used = 0, resume_limit_reset = ? WHERE user_id = ?`, [resetDate.toISOString(), userId] ); userProfile.resume_optimizations_used = 0; } const totalLimit = userWeeklyLimit + (userProfile.resume_booster_count || 0); const remainingOptimizations = totalLimit - userProfile.resume_optimizations_used; res.json({ remainingOptimizations, resetDate }); } catch (err) { console.error('Error fetching remaining optimizations:', err); res.status(500).json({ error: 'Failed to fetch remaining optimizations.' }); } } ); // Helper function to get the week number function getWeekNumber(date) { const oneJan = new Date(date.getFullYear(), 0, 1); const numberOfDays = Math.floor((date - oneJan) / (24 * 60 * 60 * 1000)); return Math.ceil((date.getDay() + 1 + numberOfDays) / 7); } /* ------------------------------------------------------------------ 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}`); });