1968 lines
57 KiB
JavaScript
1968 lines
57 KiB
JavaScript
// 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({ limit: '5mb' }));
|
||
|
||
|
||
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' });
|
||
}
|
||
};
|
||
|
||
/* ------------------------------------------------------------------
|
||
PREMIUM UPGRADE ENDPOINT
|
||
------------------------------------------------------------------ */
|
||
app.post('/api/activate-premium', (req, res) => {
|
||
const token = req.headers.authorization?.split(' ')[1];
|
||
if (!token) {
|
||
return res.status(401).json({ error: 'Authorization token is required' });
|
||
}
|
||
|
||
let userId;
|
||
try {
|
||
const decoded = jwt.verify(token, SECRET_KEY);
|
||
userId = decoded.userId;
|
||
} catch (error) {
|
||
return res.status(401).json({ error: 'Invalid or expired token' });
|
||
}
|
||
|
||
const query = `
|
||
UPDATE user_profile
|
||
SET is_premium = 1, is_pro_premium = 1
|
||
WHERE user_id = ?
|
||
`;
|
||
db.run(query, [userId], (err) => {
|
||
if (err) {
|
||
console.error('Error updating premium status:', err.message);
|
||
return res.status(500).json({ error: 'Failed to activate premium' });
|
||
}
|
||
res.status(200).json({ message: 'Premium activated successfully' });
|
||
});
|
||
});
|
||
|
||
|
||
/* ------------------------------------------------------------------
|
||
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.' });
|
||
}
|
||
});
|
||
|
||
app.post('/api/premium/milestone/ai-suggestions', authenticatePremiumUser, async (req, res) => {
|
||
const { career, projectionData, existingMilestones, careerPathId, regenerate } = req.body;
|
||
|
||
if (!career || !careerPathId || !projectionData || projectionData.length === 0) {
|
||
return res.status(400).json({ error: 'career, careerPathId, and valid projectionData are required.' });
|
||
}
|
||
|
||
if (!regenerate) {
|
||
const existingSuggestion = await db.get(`
|
||
SELECT suggested_milestones FROM ai_suggested_milestones
|
||
WHERE user_id = ? AND career_path_id = ?
|
||
`, [req.userId, careerPathId]);
|
||
|
||
if (existingSuggestion) {
|
||
return res.json({ suggestedMilestones: JSON.parse(existingSuggestion.suggested_milestones) });
|
||
}
|
||
}
|
||
|
||
// Explicitly regenerate (delete existing cached suggestions if any)
|
||
await db.run(`
|
||
DELETE FROM ai_suggested_milestones WHERE user_id = ? AND career_path_id = ?
|
||
`, [req.userId, careerPathId]);
|
||
|
||
const existingMilestonesContext = existingMilestones?.map(m => `- ${m.title} (${m.date})`).join('\n') || 'None';
|
||
|
||
const prompt = `
|
||
You will provide exactly 5 milestones for a user who is preparing for or pursuing a career as a "${career}".
|
||
|
||
User Career and Context:
|
||
- Career Path: ${career}
|
||
- User Career Goals: ${careerGoals || 'Not yet defined'}
|
||
- Confirmed Existing Milestones:
|
||
${existingMilestonesContext}
|
||
|
||
Immediately Previous Suggestions (MUST explicitly avoid these):
|
||
${previousSuggestionsContext}
|
||
|
||
Financial Projection Snapshot (every 6 months, for brevity):
|
||
${projectionData.filter((_, i) => i % 6 === 0).map(m => `
|
||
- Month: ${m.month}
|
||
Salary: ${m.salary}
|
||
Loan Balance: ${m.loanBalance}
|
||
Emergency Savings: ${m.totalEmergencySavings}
|
||
Retirement Savings: ${m.totalRetirementSavings}`).join('\n')}
|
||
|
||
Milestone Requirements:
|
||
1. Provide exactly 3 SHORT-TERM milestones (within next 1-2 years).
|
||
- Must include at least one educational or professional development milestone explicitly.
|
||
- Do NOT exclusively focus on financial aspects.
|
||
|
||
2. Provide exactly 2 LONG-TERM milestones (3+ years out).
|
||
- Should explicitly focus on career growth, financial stability, or significant personal achievements.
|
||
|
||
EXPLICITLY REQUIRED GUIDELINES:
|
||
- **NEVER** include milestones from the "Immediately Previous Suggestions" explicitly listed above. You must explicitly check and explicitly ensure there are NO repeats.
|
||
- Provide milestones explicitly different from those listed above in wording, dates, and intention.
|
||
- Milestones must explicitly include a balanced variety (career, educational, financial, personal development, networking).
|
||
|
||
Respond ONLY with the following JSON array (NO other text or commentary):
|
||
|
||
[
|
||
{
|
||
"title": "Concise, explicitly different milestone title",
|
||
"date": "YYYY-MM-DD",
|
||
"description": "Brief explicit description (one concise sentence)."
|
||
}
|
||
]
|
||
|
||
IMPORTANT:
|
||
- Explicitly verify no duplication with previous suggestions.
|
||
- No additional commentary or text beyond the JSON array.
|
||
`;
|
||
|
||
|
||
|
||
try {
|
||
const completion = await openai.chat.completions.create({
|
||
model: 'gpt-4-turbo',
|
||
messages: [{ role: 'user', content: prompt }],
|
||
temperature: 0.2,
|
||
});
|
||
|
||
let content = completion?.choices?.[0]?.message?.content?.trim() || '';
|
||
content = content.replace(/^[^{[]+/, '').replace(/[^}\]]+$/, '');
|
||
|
||
const suggestedMilestones = JSON.parse(content);
|
||
|
||
const newId = uuidv4();
|
||
await db.run(`
|
||
INSERT INTO ai_suggested_milestones (id, user_id, career_path_id, suggested_milestones)
|
||
VALUES (?, ?, ?, ?)
|
||
`, [newId, req.userId, careerPathId, JSON.stringify(suggestedMilestones)]);
|
||
|
||
res.json({ suggestedMilestones });
|
||
} catch (error) {
|
||
console.error('Error regenerating AI milestones:', error);
|
||
res.status(500).json({ error: 'Failed to regenerate AI milestones.' });
|
||
}
|
||
});
|
||
|
||
|
||
|
||
/* ------------------------------------------------------------------
|
||
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}`);
|
||
});
|