dev1/backend/server3.js

1968 lines
57 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 scenarios 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}`);
});