596 lines
21 KiB
JavaScript
596 lines
21 KiB
JavaScript
// server3.js - Premium Services API
|
|
import express from 'express';
|
|
import cors from 'cors';
|
|
import helmet from 'helmet';
|
|
import dotenv from 'dotenv';
|
|
import { open } from 'sqlite';
|
|
import sqlite3 from 'sqlite3';
|
|
import jwt from 'jsonwebtoken';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
import path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
import { simulateFinancialProjection } from '../src/utils/FinancialProjectionService.js';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
dotenv.config({ path: path.resolve(__dirname, '..', '.env') });
|
|
|
|
const app = express();
|
|
const PORT = process.env.PREMIUM_PORT || 5002;
|
|
|
|
let db;
|
|
const initDB = async () => {
|
|
try {
|
|
db = await open({
|
|
filename: '/home/jcoakley/aptiva-dev1-app/user_profile.db',
|
|
driver: sqlite3.Database
|
|
});
|
|
console.log('Connected to user_profile.db for Premium Services.');
|
|
} catch (error) {
|
|
console.error('Error connecting to premium database:', error);
|
|
}
|
|
};
|
|
initDB();
|
|
|
|
app.use(helmet());
|
|
app.use(express.json());
|
|
|
|
const allowedOrigins = ['https://dev1.aptivaai.com'];
|
|
app.use(cors({ origin: allowedOrigins, credentials: true }));
|
|
|
|
const authenticatePremiumUser = (req, res, next) => {
|
|
const token = req.headers.authorization?.split(' ')[1];
|
|
if (!token) return res.status(401).json({ error: 'Premium authorization required' });
|
|
|
|
try {
|
|
const SECRET_KEY = process.env.SECRET_KEY || 'supersecurekey';
|
|
const { userId } = jwt.verify(token, SECRET_KEY);
|
|
req.userId = userId;
|
|
next();
|
|
} catch (error) {
|
|
return res.status(403).json({ error: 'Invalid or expired token' });
|
|
}
|
|
};
|
|
|
|
// Get latest selected planned path
|
|
app.get('/api/premium/planned-path/latest', authenticatePremiumUser, async (req, res) => {
|
|
try {
|
|
const row = await db.get(
|
|
`SELECT * FROM career_path WHERE user_id = ? ORDER BY start_date DESC LIMIT 1`,
|
|
[req.userId]
|
|
);
|
|
res.json(row || {});
|
|
} catch (error) {
|
|
console.error('Error fetching latest career path:', error);
|
|
res.status(500).json({ error: 'Failed to fetch latest planned path' });
|
|
}
|
|
});
|
|
|
|
// Get all planned paths for the user
|
|
app.get('/api/premium/planned-path/all', authenticatePremiumUser, async (req, res) => {
|
|
try {
|
|
const rows = await db.all(
|
|
`SELECT * FROM career_path WHERE user_id = ? ORDER BY start_date ASC`,
|
|
[req.userId]
|
|
);
|
|
res.json({ careerPath: rows });
|
|
} catch (error) {
|
|
console.error('Error fetching career paths:', error);
|
|
res.status(500).json({ error: 'Failed to fetch planned paths' });
|
|
}
|
|
});
|
|
|
|
// Save a new planned path
|
|
app.post('/api/premium/planned-path', authenticatePremiumUser, async (req, res) => {
|
|
let { career_name } = req.body;
|
|
|
|
if (!career_name) {
|
|
return res.status(400).json({ error: 'Career name is required.' });
|
|
}
|
|
|
|
try {
|
|
// Ensure that career_name is always a string
|
|
if (typeof career_name !== 'string') {
|
|
console.warn('career_name was not a string. Converting to string.');
|
|
career_name = String(career_name); // Convert to string
|
|
}
|
|
|
|
// Check if the career path already exists for the user
|
|
const existingCareerPath = await db.get(
|
|
`SELECT id FROM career_path WHERE user_id = ? AND career_name = ?`,
|
|
[req.userId, career_name]
|
|
);
|
|
|
|
if (existingCareerPath) {
|
|
return res.status(200).json({
|
|
message: 'Career path already exists. Would you like to reload it or create a new one?',
|
|
career_path_id: existingCareerPath.id,
|
|
action_required: 'reload_or_create'
|
|
});
|
|
}
|
|
|
|
// Define a new career path id and insert into the database
|
|
const newCareerPathId = uuidv4();
|
|
await db.run(
|
|
`INSERT INTO career_path (id, user_id, career_name) VALUES (?, ?, ?)`,
|
|
[newCareerPathId, req.userId, career_name]
|
|
);
|
|
|
|
res.status(201).json({
|
|
message: 'Career path saved.',
|
|
career_path_id: newCareerPathId,
|
|
action_required: 'new_created'
|
|
});
|
|
} catch (error) {
|
|
console.error('Error saving career path:', error);
|
|
res.status(500).json({ error: 'Failed to save career path.' });
|
|
}
|
|
});
|
|
|
|
|
|
// Milestones premium services
|
|
// Save a new milestone
|
|
app.post('/api/premium/milestone', authenticatePremiumUser, async (req, res) => {
|
|
const rawMilestones = Array.isArray(req.body.milestones) ? req.body.milestones : [req.body];
|
|
|
|
const errors = [];
|
|
const validMilestones = [];
|
|
|
|
for (const [index, m] of rawMilestones.entries()) {
|
|
const {
|
|
milestone_type,
|
|
title,
|
|
description,
|
|
date,
|
|
career_path_id,
|
|
salary_increase,
|
|
status = 'planned',
|
|
date_completed = null,
|
|
context_snapshot = null,
|
|
progress = 0,
|
|
} = m;
|
|
|
|
// Validate required fields
|
|
if (!milestone_type || !title || !description || !date || !career_path_id) {
|
|
errors.push({
|
|
index,
|
|
error: 'Missing required fields',
|
|
title, // <-- Add the title for identification
|
|
date,
|
|
details: {
|
|
milestone_type: !milestone_type ? 'Required' : undefined,
|
|
title: !title ? 'Required' : undefined,
|
|
description: !description ? 'Required' : undefined,
|
|
date: !date ? 'Required' : undefined,
|
|
career_path_id: !career_path_id ? 'Required' : undefined,
|
|
}
|
|
});
|
|
continue;
|
|
}
|
|
|
|
validMilestones.push({
|
|
id: uuidv4(), // ✅ assign UUID for unique milestone ID
|
|
user_id: req.userId,
|
|
milestone_type,
|
|
title,
|
|
description,
|
|
date,
|
|
career_path_id,
|
|
salary_increase: salary_increase || null,
|
|
status,
|
|
date_completed,
|
|
context_snapshot,
|
|
progress
|
|
});
|
|
}
|
|
|
|
if (errors.length) {
|
|
console.warn('❗ Some milestones failed validation. Logging malformed records...');
|
|
console.warn(JSON.stringify(errors, null, 2));
|
|
|
|
return res.status(400).json({
|
|
error: 'Some milestones are invalid',
|
|
errors
|
|
});
|
|
}
|
|
|
|
try {
|
|
const insertPromises = validMilestones.map(m =>
|
|
db.run(
|
|
`INSERT INTO milestones (
|
|
id, user_id, milestone_type, title, description, date, career_path_id,
|
|
salary_increase, status, date_completed, context_snapshot, progress, updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
|
|
[
|
|
m.id, m.user_id, m.milestone_type, m.title, m.description, m.date, m.career_path_id,
|
|
m.salary_increase, m.status, m.date_completed, m.context_snapshot, m.progress
|
|
]
|
|
)
|
|
);
|
|
|
|
await Promise.all(insertPromises);
|
|
|
|
res.status(201).json({ message: 'Milestones saved successfully', count: validMilestones.length });
|
|
} catch (error) {
|
|
console.error('Error saving milestones:', error);
|
|
res.status(500).json({ error: 'Failed to save milestones' });
|
|
}
|
|
});
|
|
|
|
// Get all milestones
|
|
app.get('/api/premium/milestones', authenticatePremiumUser, async (req, res) => {
|
|
try {
|
|
const { careerPathId } = req.query;
|
|
|
|
if (!careerPathId) {
|
|
return res.status(400).json({ error: 'careerPathId is required' });
|
|
}
|
|
|
|
const milestones = await db.all(
|
|
`SELECT * FROM milestones WHERE user_id = ? AND career_path_id = ? ORDER BY date ASC`,
|
|
[req.userId, careerPathId]
|
|
);
|
|
|
|
res.json({ milestones });
|
|
} catch (error) {
|
|
console.error('Error fetching milestones:', error);
|
|
res.status(500).json({ error: 'Failed to fetch milestones' });
|
|
}
|
|
});
|
|
|
|
// Update an existing milestone
|
|
app.put('/api/premium/milestones/:id', authenticatePremiumUser, async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const numericId = parseInt(id, 10); // 👈 Block-defined for SQLite safety
|
|
|
|
if (isNaN(numericId)) {
|
|
return res.status(400).json({ error: 'Invalid milestone ID' });
|
|
}
|
|
|
|
const {
|
|
milestone_type,
|
|
title,
|
|
description,
|
|
date,
|
|
progress,
|
|
status,
|
|
date_completed,
|
|
salary_increase,
|
|
context_snapshot,
|
|
} = req.body;
|
|
|
|
// Explicit required field validation
|
|
if (!milestone_type || !title || !description || !date || progress === undefined) {
|
|
return res.status(400).json({
|
|
error: 'Missing required fields',
|
|
details: {
|
|
milestone_type: !milestone_type ? 'Required' : undefined,
|
|
title: !title ? 'Required' : undefined,
|
|
description: !description ? 'Required' : undefined,
|
|
date: !date ? 'Required' : undefined,
|
|
progress: progress === undefined ? 'Required' : undefined,
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
console.log('Updating milestone with:', {
|
|
milestone_type,
|
|
title,
|
|
description,
|
|
date,
|
|
progress,
|
|
status,
|
|
date_completed,
|
|
salary_increase,
|
|
context_snapshot,
|
|
id: numericId,
|
|
userId: req.userId
|
|
});
|
|
|
|
await db.run(
|
|
`UPDATE milestones SET
|
|
milestone_type = ?, title = ?, description = ?, date = ?, progress = ?,
|
|
status = ?, date_completed = ?, salary_increase = ?, context_snapshot = ?,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ? AND user_id = ?`,
|
|
[
|
|
milestone_type,
|
|
title,
|
|
description,
|
|
date,
|
|
progress || 0,
|
|
status || 'planned',
|
|
date_completed,
|
|
salary_increase || null,
|
|
context_snapshot || null,
|
|
numericId, // 👈 used here in the query
|
|
req.userId
|
|
]
|
|
);
|
|
|
|
res.status(200).json({ message: 'Milestone updated successfully' });
|
|
} catch (error) {
|
|
console.error('Error updating milestone:', error.message, error.stack);
|
|
res.status(500).json({ error: 'Failed to update milestone' });
|
|
}
|
|
});
|
|
|
|
|
|
app.delete('/api/premium/milestones/:id', authenticatePremiumUser, async (req, res) => {
|
|
const { id } = req.params;
|
|
try {
|
|
await db.run(`DELETE FROM milestones WHERE id = ? AND user_id = ?`, [id, req.userId]);
|
|
res.status(200).json({ message: 'Milestone deleted successfully' });
|
|
} catch (error) {
|
|
console.error('Error deleting milestone:', error);
|
|
res.status(500).json({ error: 'Failed to delete milestone' });
|
|
}
|
|
});
|
|
|
|
//Financial Profile premium services
|
|
//Get financial profile
|
|
app.get('/api/premium/financial-profile', authenticatePremiumUser, async (req, res) => {
|
|
try {
|
|
const row = await db.get(`SELECT * FROM financial_profile 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' });
|
|
}
|
|
});
|
|
|
|
// Backend code (server3.js)
|
|
|
|
// Save or update financial profile
|
|
app.post('/api/premium/financial-profile', authenticatePremiumUser, async (req, res) => {
|
|
const {
|
|
currentSalary, additionalIncome, monthlyExpenses, monthlyDebtPayments,
|
|
retirementSavings, retirementContribution, emergencyFund,
|
|
inCollege, expectedGraduation, partTimeIncome, tuitionPaid, collegeLoanTotal,
|
|
selectedSchool, selectedProgram, programType, isFullyOnline, creditHoursPerYear, hoursCompleted,
|
|
careerPathId, loanDeferralUntilGraduation, tuition, programLength, interestRate, loanTerm, extraPayment, expectedSalary
|
|
} = req.body;
|
|
|
|
try {
|
|
// **Call the simulateFinancialProjection function here** with all the incoming data
|
|
const { projectionData, loanPaidOffMonth } = simulateFinancialProjection({
|
|
currentSalary: req.body.currentSalary + (req.body.additionalIncome || 0),
|
|
monthlyExpenses: req.body.monthlyExpenses,
|
|
monthlyDebtPayments: req.body.monthlyDebtPayments || 0,
|
|
studentLoanAmount: req.body.collegeLoanTotal,
|
|
|
|
// ✅ UPDATED to dynamic fields from frontend
|
|
interestRate: req.body.interestRate,
|
|
loanTerm: req.body.loanTerm,
|
|
extraPayment: req.body.extraPayment || 0,
|
|
expectedSalary: req.body.expectedSalary,
|
|
|
|
emergencySavings: req.body.emergencyFund,
|
|
retirementSavings: req.body.retirementSavings,
|
|
monthlyRetirementContribution: req.body.retirementContribution,
|
|
monthlyEmergencyContribution: 0,
|
|
gradDate: req.body.expectedGraduation,
|
|
fullTimeCollegeStudent: req.body.inCollege,
|
|
partTimeIncome: req.body.partTimeIncome,
|
|
startDate: new Date(),
|
|
programType: req.body.programType,
|
|
isFullyOnline: req.body.isFullyOnline,
|
|
creditHoursPerYear: req.body.creditHoursPerYear,
|
|
calculatedTuition: req.body.tuition,
|
|
manualTuition: 0,
|
|
hoursCompleted: req.body.hoursCompleted,
|
|
loanDeferralUntilGraduation: req.body.loanDeferralUntilGraduation,
|
|
programLength: req.body.programLength
|
|
});
|
|
// Now you can save the profile or update the database with the new data
|
|
const existing = await db.get(`SELECT id FROM financial_profile WHERE user_id = ?`, [req.userId]);
|
|
|
|
if (existing) {
|
|
// Updating existing profile
|
|
await db.run(`
|
|
UPDATE financial_profile SET
|
|
current_salary = ?, additional_income = ?, monthly_expenses = ?, monthly_debt_payments = ?,
|
|
retirement_savings = ?, retirement_contribution = ?, emergency_fund = ?,
|
|
in_college = ?, expected_graduation = ?, part_time_income = ?, tuition_paid = ?, college_loan_total = ?,
|
|
selected_school = ?, selected_program = ?, program_type = ?, is_online = ?, credit_hours_per_year = ?, hours_completed = ?,
|
|
tuition = ?, loan_deferral_until_graduation = ?, program_length = ?,
|
|
interest_rate = ?, loan_term = ?, extra_payment = ?, expected_salary = ?,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE user_id = ?`,
|
|
[
|
|
currentSalary, additionalIncome, monthlyExpenses, monthlyDebtPayments,
|
|
retirementSavings, retirementContribution, emergencyFund,
|
|
inCollege ? 1 : 0, expectedGraduation, partTimeIncome, tuitionPaid, collegeLoanTotal,
|
|
selectedSchool, selectedProgram, programType, isFullyOnline, creditHoursPerYear, hoursCompleted,
|
|
tuition, loanDeferralUntilGraduation, programLength,
|
|
interestRate, loanTerm, extraPayment, expectedSalary, // ✅ added new fields
|
|
req.userId
|
|
]
|
|
);
|
|
} else {
|
|
// Insert a new profile
|
|
await db.run(`
|
|
INSERT INTO financial_profile (
|
|
id, user_id, current_salary, additional_income, monthly_expenses, monthly_debt_payments,
|
|
retirement_savings, retirement_contribution, emergency_fund, in_college, expected_graduation,
|
|
part_time_income, tuition_paid, college_loan_total, selected_school, selected_program, program_type,
|
|
is_online, credit_hours_per_year, calculated_tuition, loan_deferral_until_graduation, hours_completed, tuition, program_length,
|
|
interest_rate, loan_term, extra_payment, expected_salary
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
uuidv4(), req.userId, currentSalary, additionalIncome, monthlyExpenses, monthlyDebtPayments,
|
|
retirementSavings, retirementContribution, emergencyFund,
|
|
inCollege ? 1 : 0, expectedGraduation, partTimeIncome, tuitionPaid, collegeLoanTotal,
|
|
selectedSchool, selectedProgram, programType, isFullyOnline, creditHoursPerYear, hoursCompleted,
|
|
tuition, loanDeferralUntilGraduation, programLength,
|
|
interestRate, loanTerm, extraPayment, expectedSalary // ✅ added new fields
|
|
]
|
|
);
|
|
|
|
}
|
|
|
|
// Return the financial simulation results (calculated projection data) to the frontend
|
|
res.status(200).json({
|
|
message: 'Financial profile saved.',
|
|
projectionData,
|
|
loanPaidOffMonth,
|
|
emergencyFund: emergencyFund // explicitly add the emergency fund here
|
|
});
|
|
|
|
console.log("Request body:", req.body);
|
|
|
|
} catch (error) {
|
|
console.error('Error saving financial profile:', error);
|
|
res.status(500).json({ error: 'Failed to save financial profile.' });
|
|
}
|
|
});
|
|
|
|
//PreimumOnboarding
|
|
//Career onboarding
|
|
app.post('/api/premium/onboarding/career', authenticatePremiumUser, async (req, res) => {
|
|
const { career_name, status, start_date, projected_end_date } = req.body;
|
|
|
|
try {
|
|
const careerPathId = uuidv4();
|
|
|
|
await db.run(`
|
|
INSERT INTO career_path (id, user_id, career_name, status, start_date, projected_end_date)
|
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
[careerPathId, req.userId, career_name, status || 'planned', start_date || new Date().toISOString(), projected_end_date || null]
|
|
);
|
|
|
|
res.status(201).json({ message: 'Career onboarding data saved.', careerPathId });
|
|
} catch (error) {
|
|
console.error('Error saving career onboarding data:', error);
|
|
res.status(500).json({ error: 'Failed to save career onboarding data.' });
|
|
}
|
|
});
|
|
|
|
//Financial onboarding
|
|
app.post('/api/premium/onboarding/financial', authenticatePremiumUser, async (req, res) => {
|
|
const {
|
|
current_salary, additional_income, monthly_expenses, monthly_debt_payments,
|
|
retirement_savings, retirement_contribution, emergency_fund
|
|
} = req.body;
|
|
|
|
try {
|
|
await db.run(`
|
|
INSERT INTO financial_profile (
|
|
id, user_id, current_salary, additional_income, monthly_expenses,
|
|
monthly_debt_payments, retirement_savings, retirement_contribution, emergency_fund, updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
|
|
[
|
|
uuidv4(), req.userId, current_salary, additional_income, monthly_expenses,
|
|
monthly_debt_payments, retirement_savings, retirement_contribution, emergency_fund
|
|
]
|
|
);
|
|
|
|
res.status(201).json({ message: 'Financial onboarding data saved.' });
|
|
} catch (error) {
|
|
console.error('Error saving financial onboarding data:', error);
|
|
res.status(500).json({ error: 'Failed to save financial onboarding data.' });
|
|
}
|
|
});
|
|
|
|
//College onboarding
|
|
app.post('/api/premium/onboarding/college', authenticatePremiumUser, async (req, res) => {
|
|
const {
|
|
in_college, expected_graduation, selected_school, selected_program,
|
|
program_type, is_online, credit_hours_per_year, hours_completed
|
|
} = req.body;
|
|
|
|
try {
|
|
await db.run(`
|
|
INSERT INTO financial_profile (
|
|
id, user_id, in_college, expected_graduation, selected_school,
|
|
selected_program, program_type, is_online, credit_hours_per_year,
|
|
hours_completed, updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`,
|
|
[
|
|
uuidv4(), req.userId, in_college ? 1 : 0, expected_graduation, selected_school,
|
|
selected_program, program_type, is_online, credit_hours_per_year,
|
|
hours_completed
|
|
]
|
|
);
|
|
|
|
res.status(201).json({ message: 'College onboarding data saved.' });
|
|
} catch (error) {
|
|
console.error('Error saving college onboarding data:', error);
|
|
res.status(500).json({ error: 'Failed to save college onboarding data.' });
|
|
}
|
|
});
|
|
|
|
//Financial Projection Premium Services
|
|
// Save financial projection for a specific careerPathId
|
|
app.post('/api/premium/financial-projection/:careerPathId', authenticatePremiumUser, async (req, res) => {
|
|
const { careerPathId } = req.params;
|
|
const { projectionData } = req.body; // JSON containing detailed financial projections
|
|
|
|
try {
|
|
const projectionId = uuidv4();
|
|
|
|
await db.run(`
|
|
INSERT INTO financial_projections (id, user_id, career_path_id, projection_json)
|
|
VALUES (?, ?, ?, ?)`,
|
|
[projectionId, req.userId, careerPathId, JSON.stringify(projectionData)]
|
|
);
|
|
|
|
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.' });
|
|
}
|
|
});
|
|
|
|
// Get financial projection for a specific careerPathId
|
|
app.get('/api/premium/financial-projection/:careerPathId', authenticatePremiumUser, async (req, res) => {
|
|
const { careerPathId } = req.params;
|
|
|
|
try {
|
|
const projection = await db.get(`
|
|
SELECT projection_json FROM financial_projections
|
|
WHERE user_id = ? AND career_path_id = ?`,
|
|
[req.userId, careerPathId]
|
|
);
|
|
|
|
if (!projection) {
|
|
return res.status(404).json({ error: 'Projection not found.' });
|
|
}
|
|
|
|
res.status(200).json(JSON.parse(projection.projection_json));
|
|
} catch (error) {
|
|
console.error('Error fetching financial projection:', error);
|
|
res.status(500).json({ error: 'Failed to fetch financial projection.' });
|
|
}
|
|
});
|
|
|
|
// ROI Analysis (placeholder logic)
|
|
app.get('/api/premium/roi-analysis', authenticatePremiumUser, async (req, res) => {
|
|
try {
|
|
const userCareer = await db.get(
|
|
`SELECT * FROM career_path WHERE user_id = ? ORDER BY start_date DESC LIMIT 1`,
|
|
[req.userId]
|
|
);
|
|
|
|
if (!userCareer) return res.status(404).json({ error: 'No planned path found for user' });
|
|
|
|
const roi = {
|
|
jobTitle: userCareer.job_title,
|
|
salary: userCareer.salary,
|
|
tuition: 50000,
|
|
netGain: userCareer.salary - 50000
|
|
};
|
|
|
|
res.json(roi);
|
|
} catch (error) {
|
|
console.error('Error calculating ROI:', error);
|
|
res.status(500).json({ error: 'Failed to calculate ROI' });
|
|
}
|
|
});
|
|
|
|
app.listen(PORT, () => {
|
|
console.log(`Premium server running on http://localhost:${PORT}`);
|
|
}); |