Fixed tax implementation in simulator, milestone impacts.
This commit is contained in:
parent
253dbee9fe
commit
8d1dcf26b9
@ -3,7 +3,7 @@ import React, { useEffect, useState, useCallback } from 'react';
|
||||
|
||||
const today = new Date();
|
||||
|
||||
const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView }) => {
|
||||
const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView, onMilestoneUpdated }) => {
|
||||
const [milestones, setMilestones] = useState({ Career: [], Financial: [] });
|
||||
|
||||
// The "new or edit" milestone form state
|
||||
@ -148,6 +148,7 @@ const MilestoneTimeline = ({ careerPathId, authFetch, activeView, setActiveView
|
||||
alert(errorData.error || 'Error saving milestone');
|
||||
return;
|
||||
}
|
||||
if (onMilestoneUpdated) onMilestoneUpdated();
|
||||
|
||||
const savedMilestone = await res.json();
|
||||
console.log('Milestone saved/updated:', savedMilestone);
|
||||
|
@ -1,51 +1,74 @@
|
||||
// src/components/MilestoneTracker.js
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Line } from 'react-chartjs-2';
|
||||
import { Chart as ChartJS, LineElement, CategoryScale, LinearScale, PointElement, Tooltip, Legend } from 'chart.js';
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
LineElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
Tooltip,
|
||||
Legend
|
||||
} from 'chart.js';
|
||||
import annotationPlugin from 'chartjs-plugin-annotation';
|
||||
import { Filler } from 'chart.js';
|
||||
|
||||
import authFetch from '../utils/authFetch.js';
|
||||
import CareerSelectDropdown from './CareerSelectDropdown.js';
|
||||
import CareerSearch from './CareerSearch.js';
|
||||
import MilestoneTimeline from './MilestoneTimeline.js';
|
||||
import AISuggestedMilestones from './AISuggestedMilestones.js';
|
||||
import ScenarioEditModal from './ScenarioEditModal.js';
|
||||
import './MilestoneTracker.css';
|
||||
import './MilestoneTimeline.css';
|
||||
import { simulateFinancialProjection } from '../utils/FinancialProjectionService.js';
|
||||
import ScenarioEditModal from './ScenarioEditModal.js';
|
||||
|
||||
ChartJS.register(LineElement, CategoryScale, LinearScale, Filler, PointElement, Tooltip, Legend, annotationPlugin);
|
||||
ChartJS.register(
|
||||
LineElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
Filler,
|
||||
PointElement,
|
||||
Tooltip,
|
||||
Legend,
|
||||
annotationPlugin
|
||||
);
|
||||
|
||||
const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const apiURL = process.env.REACT_APP_API_URL;
|
||||
|
||||
// -------------------------
|
||||
// State
|
||||
// -------------------------
|
||||
const [selectedCareer, setSelectedCareer] = useState(initialCareer || null);
|
||||
const [careerPathId, setCareerPathId] = useState(null);
|
||||
const [existingCareerPaths, setExistingCareerPaths] = useState([]);
|
||||
const [pendingCareerForModal, setPendingCareerForModal] = useState(null);
|
||||
const [activeView, setActiveView] = useState("Career");
|
||||
|
||||
// Store each profile separately
|
||||
const [financialProfile, setFinancialProfile] = useState(null);
|
||||
const [collegeProfile, setCollegeProfile] = useState(null);
|
||||
|
||||
// For the chart
|
||||
const [projectionData, setProjectionData] = useState([]);
|
||||
const [loanPayoffMonth, setLoanPayoffMonth] = useState(null);
|
||||
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
|
||||
const apiURL = process.env.REACT_APP_API_URL;
|
||||
|
||||
// Possibly loaded from location.state
|
||||
const { projectionData: initialProjectionData = [], loanPayoffMonth: initialLoanPayoffMonth = null } = location.state || {};
|
||||
const {
|
||||
projectionData: initialProjectionData = [],
|
||||
loanPayoffMonth: initialLoanPayoffMonth = null
|
||||
} = location.state || {};
|
||||
|
||||
// ----------------------------
|
||||
// 1. Fetch career paths + financialProfile
|
||||
// ----------------------------
|
||||
// -------------------------
|
||||
// 1. Fetch career paths + financialProfile on mount
|
||||
// -------------------------
|
||||
useEffect(() => {
|
||||
const fetchCareerPaths = async () => {
|
||||
const res = await authFetch(`${apiURL}/premium/career-profile/all`);
|
||||
@ -58,6 +81,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
||||
setSelectedCareer(fromPopout);
|
||||
setCareerPathId(fromPopout.career_path_id);
|
||||
} else if (!selectedCareer) {
|
||||
// Try to fetch the latest
|
||||
const latest = await authFetch(`${apiURL}/premium/career-profile/latest`);
|
||||
if (latest && latest.ok) {
|
||||
const latestData = await latest.json();
|
||||
@ -81,9 +105,9 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
||||
fetchFinancialProfile();
|
||||
}, [apiURL, location.state, selectedCareer]);
|
||||
|
||||
// ----------------------------
|
||||
// -------------------------
|
||||
// 2. Fetch the college profile for the selected careerPathId
|
||||
// ----------------------------
|
||||
// -------------------------
|
||||
useEffect(() => {
|
||||
if (!careerPathId) {
|
||||
setCollegeProfile(null);
|
||||
@ -91,109 +115,234 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
||||
}
|
||||
|
||||
const fetchCollegeProfile = async () => {
|
||||
// If you have a route like GET /api/premium/college-profile?careerPathId=XYZ
|
||||
const res = await authFetch(`${apiURL}/premium/college-profile?careerPathId=${careerPathId}`);
|
||||
if (!res || !res.ok) {
|
||||
setCollegeProfile(null);
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
setCollegeProfile(data); // could be an object or empty {}
|
||||
setCollegeProfile(data);
|
||||
};
|
||||
|
||||
fetchCollegeProfile();
|
||||
}, [careerPathId, apiURL]);
|
||||
|
||||
// ----------------------------
|
||||
// 3. Merge data + simulate once both profiles + selectedCareer are loaded
|
||||
// ----------------------------
|
||||
// -------------------------
|
||||
// 3. Initial simulation when profiles + career loaded
|
||||
// (But this does NOT update after milestone changes yet)
|
||||
// -------------------------
|
||||
useEffect(() => {
|
||||
if (!financialProfile || !collegeProfile || !selectedCareer) return;
|
||||
console.log("About to build mergedProfile");
|
||||
console.log("collegeProfile from DB/fetch = ", collegeProfile);
|
||||
console.log(
|
||||
"college_enrollment_status check:",
|
||||
"[" + collegeProfile.college_enrollment_status + "]",
|
||||
"length=", collegeProfile.college_enrollment_status?.length
|
||||
);
|
||||
console.log(
|
||||
"Comparison => ",
|
||||
collegeProfile.college_enrollment_status === 'currently_enrolled'
|
||||
);
|
||||
if (!financialProfile || !collegeProfile || !selectedCareer || !careerPathId) return;
|
||||
|
||||
// 1) Fetch the raw milestones for this careerPath
|
||||
(async () => {
|
||||
try {
|
||||
const milRes = await authFetch(`${apiURL}/premium/milestones?careerPathId=${careerPathId}`);
|
||||
if (!milRes.ok) {
|
||||
console.error('Failed to fetch initial milestones');
|
||||
return;
|
||||
}
|
||||
const milestonesData = await milRes.json();
|
||||
const allMilestones = milestonesData.milestones || [];
|
||||
|
||||
// 2) For each milestone, fetch impacts
|
||||
const impactPromises = allMilestones.map((m) =>
|
||||
authFetch(`${apiURL}/premium/milestone-impacts?milestone_id=${m.id}`)
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((data) => data?.impacts || [])
|
||||
.catch((err) => {
|
||||
console.error('Failed fetching impacts for milestone', m.id, err);
|
||||
return [];
|
||||
})
|
||||
);
|
||||
const impactsForEach = await Promise.all(impactPromises);
|
||||
const milestonesWithImpacts = allMilestones.map((m, i) => ({
|
||||
...m,
|
||||
impacts: impactsForEach[i] || [],
|
||||
}));
|
||||
|
||||
// 3) Flatten them
|
||||
const allImpacts = milestonesWithImpacts.flatMap((m) => m.impacts || []);
|
||||
|
||||
// 4) Build the mergedProfile (like you already do)
|
||||
const mergedProfile = {
|
||||
// From financialProfile
|
||||
currentSalary: financialProfile.current_salary || 0,
|
||||
monthlyExpenses: financialProfile.monthly_expenses || 0,
|
||||
monthlyDebtPayments: financialProfile.monthly_debt_payments || 0,
|
||||
retirementSavings: financialProfile.retirement_savings || 0,
|
||||
emergencySavings: financialProfile.emergency_fund || 0,
|
||||
monthlyRetirementContribution: financialProfile.retirement_contribution || 0,
|
||||
monthlyEmergencyContribution: financialProfile.emergency_contribution || 0,
|
||||
surplusEmergencyAllocation: financialProfile.extra_cash_emergency_pct || 50,
|
||||
surplusRetirementAllocation: financialProfile.extra_cash_retirement_pct || 50,
|
||||
|
||||
// From collegeProfile
|
||||
studentLoanAmount: collegeProfile.existing_college_debt || 0,
|
||||
interestRate: collegeProfile.interest_rate || 5,
|
||||
loanTerm: collegeProfile.loan_term || 10,
|
||||
loanDeferralUntilGraduation: !!collegeProfile.loan_deferral_until_graduation,
|
||||
academicCalendar: collegeProfile.academic_calendar || 'monthly',
|
||||
annualFinancialAid: collegeProfile.annual_financial_aid || 0,
|
||||
calculatedTuition: collegeProfile.tuition || 0,
|
||||
extraPayment: collegeProfile.extra_payment || 0,
|
||||
inCollege:
|
||||
collegeProfile.college_enrollment_status === 'currently_enrolled' ||
|
||||
collegeProfile.college_enrollment_status === 'prospective_student',
|
||||
gradDate: collegeProfile.expected_graduation || null,
|
||||
programType: collegeProfile.program_type,
|
||||
creditHoursPerYear: collegeProfile.credit_hours_per_year || 0,
|
||||
hoursCompleted: collegeProfile.hours_completed || 0,
|
||||
programLength: collegeProfile.program_length || 0,
|
||||
startDate: new Date().toISOString(),
|
||||
expectedSalary: collegeProfile.expected_salary || financialProfile.current_salary,
|
||||
|
||||
// The key: impacts
|
||||
milestoneImpacts: allImpacts
|
||||
};
|
||||
|
||||
// 5) Run the simulation
|
||||
const { projectionData: initialProjData, loanPaidOffMonth: payoff } =
|
||||
simulateFinancialProjection(mergedProfile);
|
||||
|
||||
let cumulativeSavings = mergedProfile.emergencySavings || 0;
|
||||
const finalData = initialProjData.map((month) => {
|
||||
cumulativeSavings += (month.netSavings || 0);
|
||||
return { ...month, cumulativeNetSavings: cumulativeSavings };
|
||||
});
|
||||
|
||||
setProjectionData(finalData);
|
||||
setLoanPayoffMonth(payoff);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error fetching initial milestones/impacts or simulating:', err);
|
||||
}
|
||||
})();
|
||||
}, [financialProfile, collegeProfile, selectedCareer, careerPathId]);
|
||||
|
||||
// Merge financial + college data
|
||||
const mergedProfile = {
|
||||
// From financialProfile
|
||||
currentSalary: financialProfile.current_salary || 0,
|
||||
monthlyExpenses: financialProfile.monthly_expenses || 0,
|
||||
monthlyDebtPayments: financialProfile.monthly_debt_payments || 0,
|
||||
retirementSavings: financialProfile.retirement_savings || 0,
|
||||
emergencySavings: financialProfile.emergency_fund || 0,
|
||||
monthlyRetirementContribution: financialProfile.retirement_contribution || 0,
|
||||
monthlyEmergencyContribution: financialProfile.emergency_contribution || 0,
|
||||
surplusEmergencyAllocation: financialProfile.extra_cash_emergency_pct || 50,
|
||||
surplusRetirementAllocation: financialProfile.extra_cash_retirement_pct || 50,
|
||||
// -------------------------------------------------
|
||||
// 4. reSimulate() => re-fetch everything (financial, college, milestones & impacts),
|
||||
// re-run the simulation. This is triggered AFTER user updates a milestone in MilestoneTimeline.
|
||||
// -------------------------------------------------
|
||||
const reSimulate = async () => {
|
||||
if (!careerPathId) return;
|
||||
|
||||
// From collegeProfile
|
||||
studentLoanAmount: collegeProfile.existing_college_debt || 0,
|
||||
interestRate: collegeProfile.interest_rate || 5,
|
||||
loanTerm: collegeProfile.loan_term || 10,
|
||||
loanDeferralUntilGraduation: !!collegeProfile.loan_deferral_until_graduation,
|
||||
academicCalendar: collegeProfile.academic_calendar || 'monthly',
|
||||
annualFinancialAid: collegeProfile.annual_financial_aid || 0,
|
||||
calculatedTuition: collegeProfile.tuition || 0,
|
||||
extraPayment: collegeProfile.extra_payment || 0,
|
||||
partTimeIncome: 0, // or collegeProfile.part_time_income if you store it
|
||||
gradDate: collegeProfile.expected_graduation || null,
|
||||
programType: collegeProfile.program_type,
|
||||
creditHoursPerYear: collegeProfile.credit_hours_per_year || 0,
|
||||
hoursCompleted: collegeProfile.hours_completed || 0,
|
||||
programLength: collegeProfile.program_length || 0,
|
||||
try {
|
||||
// 1) Fetch financial + college + raw milestones
|
||||
const [finResp, colResp, milResp] = await Promise.all([
|
||||
authFetch(`${apiURL}/premium/financial-profile`),
|
||||
authFetch(`${apiURL}/premium/college-profile?careerPathId=${careerPathId}`),
|
||||
authFetch(`${apiURL}/premium/milestones?careerPathId=${careerPathId}`)
|
||||
]);
|
||||
|
||||
// Are they in college?
|
||||
inCollege: (collegeProfile.college_enrollment_status === 'currently_enrolled' ||
|
||||
collegeProfile.college_enrollment_status === 'prospective_student'),
|
||||
// If they've graduated or not in college, false
|
||||
startDate: new Date().toISOString(),
|
||||
// Future logic could set expectedSalary if there's a difference
|
||||
expectedSalary: collegeProfile.expected_salary || financialProfile.current_salary,
|
||||
};
|
||||
if (!finResp.ok || !colResp.ok || !milResp.ok) {
|
||||
console.error('One reSimulate fetch failed:', finResp.status, colResp.status, milResp.status);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = simulateFinancialProjection(mergedProfile);
|
||||
console.log("mergedProfile for simulation:", mergedProfile);
|
||||
const [updatedFinancial, updatedCollege, milestonesData] = await Promise.all([
|
||||
finResp.json(),
|
||||
colResp.json(),
|
||||
milResp.json()
|
||||
]);
|
||||
|
||||
const { projectionData, loanPaidOffMonth } = result;
|
||||
|
||||
// If you want to accumulate net savings:
|
||||
let cumulativeSavings = mergedProfile.emergencySavings || 0;
|
||||
const cumulativeProjectionData = projectionData.map(month => {
|
||||
cumulativeSavings += (month.netSavings || 0);
|
||||
return { ...month, cumulativeNetSavings: cumulativeSavings };
|
||||
});
|
||||
// 2) For each milestone, fetch its impacts separately (if not already included)
|
||||
const allMilestones = milestonesData.milestones || [];
|
||||
const impactsPromises = allMilestones.map(m =>
|
||||
authFetch(`${apiURL}/premium/milestone-impacts?milestone_id=${m.id}`)
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(data => data?.impacts || [])
|
||||
.catch(err => {
|
||||
console.error('Failed fetching impacts for milestone', m.id, err);
|
||||
return [];
|
||||
})
|
||||
);
|
||||
|
||||
if (cumulativeProjectionData.length > 0) {
|
||||
setProjectionData(cumulativeProjectionData);
|
||||
setLoanPayoffMonth(loanPaidOffMonth);
|
||||
const impactsForEach = await Promise.all(impactsPromises);
|
||||
// Merge them onto the milestone array if desired
|
||||
const milestonesWithImpacts = allMilestones.map((m, i) => ({
|
||||
...m,
|
||||
impacts: impactsForEach[i] || []
|
||||
}));
|
||||
|
||||
// Flatten or gather all impacts if your simulation function needs them
|
||||
const allImpacts = milestonesWithImpacts.flatMap(m => m.impacts || []);
|
||||
|
||||
// 3) Build mergedProfile
|
||||
const mergedProfile = {
|
||||
// From updatedFinancial
|
||||
currentSalary: updatedFinancial.current_salary || 0,
|
||||
monthlyExpenses: updatedFinancial.monthly_expenses || 0,
|
||||
monthlyDebtPayments: updatedFinancial.monthly_debt_payments || 0,
|
||||
retirementSavings: updatedFinancial.retirement_savings || 0,
|
||||
emergencySavings: updatedFinancial.emergency_fund || 0,
|
||||
monthlyRetirementContribution: updatedFinancial.retirement_contribution || 0,
|
||||
monthlyEmergencyContribution: updatedFinancial.emergency_contribution || 0,
|
||||
surplusEmergencyAllocation: updatedFinancial.extra_cash_emergency_pct || 50,
|
||||
surplusRetirementAllocation: updatedFinancial.extra_cash_retirement_pct || 50,
|
||||
|
||||
// From updatedCollege
|
||||
studentLoanAmount: updatedCollege.existing_college_debt || 0,
|
||||
interestRate: updatedCollege.interest_rate || 5,
|
||||
loanTerm: updatedCollege.loan_term || 10,
|
||||
loanDeferralUntilGraduation: !!updatedCollege.loan_deferral_until_graduation,
|
||||
academicCalendar: updatedCollege.academic_calendar || 'monthly',
|
||||
annualFinancialAid: updatedCollege.annual_financial_aid || 0,
|
||||
calculatedTuition: updatedCollege.tuition || 0,
|
||||
extraPayment: updatedCollege.extra_payment || 0,
|
||||
inCollege:
|
||||
updatedCollege.college_enrollment_status === 'currently_enrolled' ||
|
||||
updatedCollege.college_enrollment_status === 'prospective_student',
|
||||
gradDate: updatedCollege.expected_graduation || null,
|
||||
programType: updatedCollege.program_type,
|
||||
creditHoursPerYear: updatedCollege.credit_hours_per_year || 0,
|
||||
hoursCompleted: updatedCollege.hours_completed || 0,
|
||||
programLength: updatedCollege.program_length || 0,
|
||||
startDate: new Date().toISOString(),
|
||||
expectedSalary: updatedCollege.expected_salary || updatedFinancial.current_salary,
|
||||
|
||||
// The key: pass the impacts to the simulation if needed
|
||||
milestoneImpacts: allImpacts
|
||||
};
|
||||
|
||||
// 4) Re-run simulation
|
||||
const { projectionData: newProjData, loanPaidOffMonth: payoff } =
|
||||
simulateFinancialProjection(mergedProfile);
|
||||
|
||||
// 5) If you track cumulative net savings:
|
||||
let cumulativeSavings = mergedProfile.emergencySavings || 0;
|
||||
const finalData = newProjData.map(month => {
|
||||
cumulativeSavings += (month.netSavings || 0);
|
||||
return { ...month, cumulativeNetSavings: cumulativeSavings };
|
||||
});
|
||||
|
||||
// 6) Update states => triggers chart refresh
|
||||
setProjectionData(finalData);
|
||||
setLoanPayoffMonth(payoff);
|
||||
|
||||
// Optionally store the new profiles in state if you like
|
||||
setFinancialProfile(updatedFinancial);
|
||||
setCollegeProfile(updatedCollege);
|
||||
|
||||
console.log('Re-simulated after Milestone update!', {
|
||||
mergedProfile,
|
||||
milestonesWithImpacts
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error in reSimulate:', err);
|
||||
}
|
||||
};
|
||||
|
||||
console.log('mergedProfile for simulation:', mergedProfile);
|
||||
|
||||
}, [financialProfile, collegeProfile, selectedCareer]);
|
||||
|
||||
// 4. The rest of your code is unchanged, e.g. handleConfirmCareerSelection, etc.
|
||||
// ...
|
||||
|
||||
|
||||
// ...
|
||||
// The rest of your component logic
|
||||
// ...
|
||||
|
||||
console.log(
|
||||
'First 5 items of projectionData:',
|
||||
Array.isArray(projectionData) ? projectionData.slice(0, 5) : 'projectionData not yet available'
|
||||
);
|
||||
|
||||
// ...
|
||||
// The remainder of your component: timeline, chart, AISuggestedMilestones, etc.
|
||||
// ...
|
||||
return (
|
||||
<div className="milestone-tracker">
|
||||
<CareerSelectDropdown
|
||||
@ -207,11 +356,13 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
||||
authFetch={authFetch}
|
||||
/>
|
||||
|
||||
{/* Pass reSimulate as onMilestoneUpdated: */}
|
||||
<MilestoneTimeline
|
||||
careerPathId={careerPathId}
|
||||
authFetch={authFetch}
|
||||
activeView={activeView}
|
||||
setActiveView={setActiveView}
|
||||
onMilestoneUpdated={reSimulate}
|
||||
/>
|
||||
|
||||
<AISuggestedMilestones
|
||||
@ -227,19 +378,19 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
||||
<h3 className="text-lg font-semibold mb-2">Financial Projection</h3>
|
||||
<Line
|
||||
data={{
|
||||
labels: projectionData.map(p => p.month),
|
||||
labels: projectionData.map((p) => p.month),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Total Savings',
|
||||
data: projectionData.map(p => p.cumulativeNetSavings),
|
||||
data: projectionData.map((p) => p.cumulativeNetSavings),
|
||||
borderColor: 'rgba(54, 162, 235, 1)',
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
fill: true
|
||||
},
|
||||
{
|
||||
label: 'Loan Balance',
|
||||
data: projectionData.map(p => p.loanBalance),
|
||||
data: projectionData.map((p) => p.loanBalance),
|
||||
borderColor: 'rgba(255, 99, 132, 1)',
|
||||
backgroundColor: 'rgba(255, 99, 132, 0.2)',
|
||||
tension: 0.4,
|
||||
@ -251,7 +402,7 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
||||
},
|
||||
{
|
||||
label: 'Retirement Savings',
|
||||
data: projectionData.map(p => p.retirementSavings),
|
||||
data: projectionData.map((p) => p.retirementSavings),
|
||||
borderColor: 'rgba(75, 192, 192, 1)',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||||
tension: 0.4,
|
||||
@ -308,23 +459,23 @@ const MilestoneTracker = ({ selectedCareer: initialCareer }) => {
|
||||
authFetch={authFetch}
|
||||
/>
|
||||
|
||||
|
||||
{/* SCENARIO EDIT MODAL */}
|
||||
<ScenarioEditModal
|
||||
show={showEditModal}
|
||||
onClose={() => setShowEditModal(false)}
|
||||
financialProfile={financialProfile}
|
||||
setFinancialProfile={setFinancialProfile}
|
||||
collegeProfile={collegeProfile}
|
||||
setCollegeProfile={setCollegeProfile}
|
||||
apiURL={apiURL}
|
||||
authFetch={authFetch}
|
||||
/>
|
||||
<ScenarioEditModal
|
||||
show={showEditModal}
|
||||
onClose={() => setShowEditModal(false)}
|
||||
financialProfile={financialProfile}
|
||||
setFinancialProfile={setFinancialProfile}
|
||||
collegeProfile={collegeProfile}
|
||||
setCollegeProfile={setCollegeProfile}
|
||||
apiURL={apiURL}
|
||||
authFetch={authFetch}
|
||||
/>
|
||||
|
||||
{pendingCareerForModal && (
|
||||
<button onClick={() => {
|
||||
// handleConfirmCareerSelection logic
|
||||
}}>
|
||||
<button
|
||||
onClick={() => {
|
||||
// handleConfirmCareerSelection logic
|
||||
}}
|
||||
>
|
||||
Confirm Career Change to {pendingCareerForModal}
|
||||
</button>
|
||||
)}
|
||||
|
@ -1,67 +1,23 @@
|
||||
import moment from 'moment';
|
||||
|
||||
/**
|
||||
* Single-filer federal tax calculation (2023).
|
||||
* Includes standard deduction ($13,850).
|
||||
*/
|
||||
/***************************************************
|
||||
* HELPER: Approx State Tax Rates
|
||||
***************************************************/
|
||||
const APPROX_STATE_TAX_RATES = {
|
||||
AL: 0.05,
|
||||
AK: 0.00,
|
||||
AZ: 0.025,
|
||||
AR: 0.05,
|
||||
CA: 0.07,
|
||||
CO: 0.045,
|
||||
CT: 0.055,
|
||||
DE: 0.05,
|
||||
FL: 0.00,
|
||||
GA: 0.05,
|
||||
HI: 0.06,
|
||||
ID: 0.058,
|
||||
IL: 0.05,
|
||||
IN: 0.035,
|
||||
IA: 0.05,
|
||||
KS: 0.05,
|
||||
KY: 0.05,
|
||||
LA: 0.04,
|
||||
ME: 0.055,
|
||||
MD: 0.05,
|
||||
MA: 0.05,
|
||||
MI: 0.0425,
|
||||
MN: 0.06,
|
||||
MS: 0.04,
|
||||
MO: 0.05,
|
||||
MT: 0.05,
|
||||
NE: 0.05,
|
||||
NV: 0.00,
|
||||
NH: 0.00, // ignoring interest/dividend nuance
|
||||
NJ: 0.057,
|
||||
NM: 0.045,
|
||||
NY: 0.06,
|
||||
NC: 0.0475,
|
||||
ND: 0.02,
|
||||
OH: 0.04,
|
||||
OK: 0.045,
|
||||
OR: 0.07,
|
||||
PA: 0.03,
|
||||
RI: 0.045,
|
||||
SC: 0.04,
|
||||
SD: 0.00,
|
||||
TN: 0.00,
|
||||
TX: 0.00,
|
||||
UT: 0.045,
|
||||
VT: 0.055,
|
||||
VA: 0.05,
|
||||
WA: 0.00,
|
||||
WV: 0.05,
|
||||
WI: 0.05,
|
||||
WY: 0.00,
|
||||
DC: 0.05
|
||||
AL: 0.05, AK: 0.00, AZ: 0.025, AR: 0.05, CA: 0.07, CO: 0.045, CT: 0.055, DE: 0.05,
|
||||
FL: 0.00, GA: 0.05, HI: 0.06, ID: 0.058, IL: 0.05, IN: 0.035, IA: 0.05, KS: 0.05,
|
||||
KY: 0.05, LA: 0.04, ME: 0.055, MD: 0.05, MA: 0.05, MI: 0.0425, MN: 0.06, MS: 0.04,
|
||||
MO: 0.05, MT: 0.05, NE: 0.05, NV: 0.00, NH: 0.00, NJ: 0.057, NM: 0.045, NY: 0.06,
|
||||
NC: 0.0475, ND: 0.02, OH: 0.04, OK: 0.045, OR: 0.07, PA: 0.03, RI: 0.045, SC: 0.04,
|
||||
SD: 0.00, TN: 0.00, TX: 0.00, UT: 0.045, VT: 0.055, VA: 0.05, WA: 0.00, WV: 0.05,
|
||||
WI: 0.05, WY: 0.00, DC: 0.05
|
||||
};
|
||||
|
||||
function calculateAnnualFederalTaxSingle(annualIncome) {
|
||||
const STANDARD_DEDUCTION_SINGLE = 13850;
|
||||
const taxableIncome = Math.max(0, annualIncome - STANDARD_DEDUCTION_SINGLE);
|
||||
|
||||
/***************************************************
|
||||
* HELPER: Federal Tax Brackets
|
||||
***************************************************/
|
||||
const STANDARD_DEDUCTION_SINGLE = 13850;
|
||||
function calculateAnnualFederalTaxSingle(annualTaxable) {
|
||||
const brackets = [
|
||||
{ limit: 11000, rate: 0.10 },
|
||||
{ limit: 44725, rate: 0.12 },
|
||||
@ -74,11 +30,10 @@ function calculateAnnualFederalTaxSingle(annualIncome) {
|
||||
|
||||
let tax = 0;
|
||||
let lastLimit = 0;
|
||||
|
||||
for (let i = 0; i < brackets.length; i++) {
|
||||
const { limit, rate } = brackets[i];
|
||||
if (taxableIncome <= limit) {
|
||||
tax += (taxableIncome - lastLimit) * rate;
|
||||
if (annualTaxable <= limit) {
|
||||
tax += (annualTaxable - lastLimit) * rate;
|
||||
break;
|
||||
} else {
|
||||
tax += (limit - lastLimit) * rate;
|
||||
@ -88,14 +43,34 @@ function calculateAnnualFederalTaxSingle(annualIncome) {
|
||||
return tax;
|
||||
}
|
||||
|
||||
function calculateAnnualStateTax(annualIncome, stateCode) {
|
||||
const rate = APPROX_STATE_TAX_RATES[stateCode] ?? 0.05;
|
||||
return annualIncome * rate;
|
||||
/***************************************************
|
||||
* HELPER: Monthly Federal Tax (no YTD)
|
||||
* We just treat (monthlyGross * 12) - standardDed
|
||||
* -> bracket -> / 12
|
||||
***************************************************/
|
||||
function calculateMonthlyFedTaxNoYTD(monthlyGross) {
|
||||
const annualGross = monthlyGross * 12;
|
||||
let annualTaxable = annualGross - STANDARD_DEDUCTION_SINGLE;
|
||||
if (annualTaxable < 0) annualTaxable = 0;
|
||||
|
||||
const annualTax = calculateAnnualFederalTaxSingle(annualTaxable);
|
||||
return annualTax / 12;
|
||||
}
|
||||
|
||||
/***************************************************
|
||||
* HELPER: Monthly State Tax (no YTD)
|
||||
* Uses GA (5%) by default if user doesn't override
|
||||
***************************************************/
|
||||
function calculateMonthlyStateTaxNoYTD(monthlyGross, stateCode = 'GA') {
|
||||
const rate = APPROX_STATE_TAX_RATES[stateCode] ?? 0.05;
|
||||
return monthlyGross * rate;
|
||||
}
|
||||
|
||||
/***************************************************
|
||||
* HELPER: Loan Payment (if not deferring)
|
||||
***************************************************/
|
||||
function calculateLoanPayment(principal, annualRate, years) {
|
||||
if (principal <= 0) return 0;
|
||||
|
||||
const monthlyRate = annualRate / 100 / 12;
|
||||
const numPayments = years * 12;
|
||||
|
||||
@ -108,35 +83,28 @@ function calculateLoanPayment(principal, annualRate, years) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main projection function with bracket-based FEDERAL + optional STATE tax logic.
|
||||
*
|
||||
* milestoneImpacts: [
|
||||
* {
|
||||
* impact_type: 'ONE_TIME' | 'MONTHLY',
|
||||
* direction: 'add' | 'subtract',
|
||||
* amount: number,
|
||||
* start_date: 'YYYY-MM-DD',
|
||||
* end_date?: 'YYYY-MM-DD' | null
|
||||
* }, ...
|
||||
* ]
|
||||
*/
|
||||
/***************************************************
|
||||
* MAIN SIMULATION FUNCTION
|
||||
***************************************************/
|
||||
export function simulateFinancialProjection(userProfile) {
|
||||
/***************************************************
|
||||
* 1) DESTRUCTURE USER PROFILE
|
||||
***************************************************/
|
||||
const {
|
||||
// Income & expenses
|
||||
// Basic incomes
|
||||
currentSalary = 0,
|
||||
monthlyExpenses = 0,
|
||||
monthlyDebtPayments = 0,
|
||||
partTimeIncome = 0,
|
||||
extraPayment = 0,
|
||||
|
||||
// Loan info
|
||||
// Student loan config
|
||||
studentLoanAmount = 0,
|
||||
interestRate = 5, // %
|
||||
loanTerm = 10, // years
|
||||
interestRate = 5,
|
||||
loanTerm = 10,
|
||||
loanDeferralUntilGraduation = false,
|
||||
|
||||
// College & tuition
|
||||
// College config
|
||||
inCollege = false,
|
||||
programType,
|
||||
hoursCompleted = 0,
|
||||
@ -147,40 +115,37 @@ export function simulateFinancialProjection(userProfile) {
|
||||
academicCalendar = 'monthly',
|
||||
annualFinancialAid = 0,
|
||||
|
||||
// Salary after graduation
|
||||
// Post-college salary
|
||||
expectedSalary = 0,
|
||||
|
||||
// Savings
|
||||
// Savings & monthly contributions
|
||||
emergencySavings = 0,
|
||||
retirementSavings = 0,
|
||||
|
||||
// Monthly contributions
|
||||
monthlyRetirementContribution = 0,
|
||||
monthlyEmergencyContribution = 0,
|
||||
|
||||
// Surplus allocation
|
||||
// Surplus distribution
|
||||
surplusEmergencyAllocation = 50,
|
||||
surplusRetirementAllocation = 50,
|
||||
|
||||
// Potential override
|
||||
// Program length override
|
||||
programLength,
|
||||
|
||||
// State code
|
||||
stateCode = 'TX',
|
||||
// State code for taxes (default to GA if not provided)
|
||||
stateCode = 'GA',
|
||||
|
||||
// Milestone impacts (with dates, add/subtract logic)
|
||||
// Financial milestone impacts
|
||||
milestoneImpacts = []
|
||||
} = userProfile;
|
||||
|
||||
// scenario start date
|
||||
const scenarioStart = startDate ? new Date(startDate) : new Date();
|
||||
/***************************************************
|
||||
* 2) CLAMP THE SCENARIO START TO MONTH-BEGIN
|
||||
***************************************************/
|
||||
const scenarioStartClamped = moment(startDate || new Date()).startOf('month');
|
||||
|
||||
// 1. Monthly loan payment if not deferring
|
||||
let monthlyLoanPayment = loanDeferralUntilGraduation
|
||||
? 0
|
||||
: calculateLoanPayment(studentLoanAmount, interestRate, loanTerm);
|
||||
|
||||
// 2. Determine credit hours
|
||||
/***************************************************
|
||||
* 3) DETERMINE PROGRAM LENGTH (credit hours)
|
||||
***************************************************/
|
||||
let requiredCreditHours = 120;
|
||||
switch (programType) {
|
||||
case "Associate's Degree":
|
||||
@ -192,17 +157,20 @@ export function simulateFinancialProjection(userProfile) {
|
||||
case "Doctoral Degree":
|
||||
requiredCreditHours = 60;
|
||||
break;
|
||||
// otherwise Bachelor's
|
||||
// else Bachelor's = 120
|
||||
}
|
||||
const remainingCreditHours = Math.max(0, requiredCreditHours - hoursCompleted);
|
||||
const dynamicProgramLength = Math.ceil(remainingCreditHours / creditHoursPerYear);
|
||||
const dynamicProgramLength = Math.ceil(
|
||||
remainingCreditHours / (creditHoursPerYear || 30)
|
||||
);
|
||||
const finalProgramLength = programLength || dynamicProgramLength;
|
||||
|
||||
// 3. Net annual tuition
|
||||
const netAnnualTuition = Math.max(0, calculatedTuition - annualFinancialAid);
|
||||
/***************************************************
|
||||
* 4) TUITION CALC: lumps, deferral, etc.
|
||||
***************************************************/
|
||||
const netAnnualTuition = Math.max(0, (calculatedTuition || 0) - (annualFinancialAid || 0));
|
||||
const totalTuitionCost = netAnnualTuition * finalProgramLength;
|
||||
|
||||
// 4. lumps
|
||||
let lumpsPerYear, lumpsSchedule;
|
||||
switch (academicCalendar) {
|
||||
case 'semester':
|
||||
@ -226,99 +194,118 @@ export function simulateFinancialProjection(userProfile) {
|
||||
const totalAcademicMonths = finalProgramLength * 12;
|
||||
const lumpAmount = totalTuitionCost / (lumpsPerYear * finalProgramLength);
|
||||
|
||||
// 5. Simulation loop
|
||||
const maxMonths = 240; // 20 years
|
||||
let date = new Date(scenarioStart);
|
||||
/***************************************************
|
||||
* 5) LOAN PAYMENT (if not deferring)
|
||||
***************************************************/
|
||||
let monthlyLoanPayment = loanDeferralUntilGraduation
|
||||
? 0
|
||||
: calculateLoanPayment(studentLoanAmount, interestRate, loanTerm);
|
||||
|
||||
/***************************************************
|
||||
* 6) SETUP FOR THE SIMULATION LOOP
|
||||
***************************************************/
|
||||
const maxMonths = 240; // 20 years
|
||||
let loanBalance = Math.max(studentLoanAmount, 0);
|
||||
let loanPaidOffMonth = null;
|
||||
|
||||
let currentEmergencySavings = emergencySavings;
|
||||
let currentRetirementSavings = retirementSavings;
|
||||
|
||||
let projectionData = [];
|
||||
|
||||
// Keep track of YTD gross & tax for reference
|
||||
let fedYTDgross = 0;
|
||||
let fedYTDtax = 0;
|
||||
let stateYTDgross = 0;
|
||||
let stateYTDtax = 0;
|
||||
|
||||
let wasInDeferral = inCollege && loanDeferralUntilGraduation;
|
||||
const graduationDateObj = gradDate ? new Date(gradDate) : null;
|
||||
const graduationDateObj = gradDate ? moment(gradDate).startOf('month') : null;
|
||||
|
||||
// For YTD taxes
|
||||
const taxStateByYear = {};
|
||||
console.log('simulateFinancialProjection - monthly tax approach');
|
||||
console.log('scenarioStartClamped:', scenarioStartClamped.format('YYYY-MM-DD'));
|
||||
|
||||
for (let month = 0; month < maxMonths; month++) {
|
||||
date.setMonth(date.getMonth() + 1);
|
||||
const currentYear = date.getFullYear();
|
||||
/***************************************************
|
||||
* 7) THE MONTHLY LOOP
|
||||
***************************************************/
|
||||
for (let monthIndex = 0; monthIndex < maxMonths; monthIndex++) {
|
||||
// date for this iteration
|
||||
const currentSimDate = scenarioStartClamped.clone().add(monthIndex, 'months');
|
||||
|
||||
// elapsed months since scenario start
|
||||
const elapsedMonths = moment(date).diff(moment(scenarioStart), 'months');
|
||||
|
||||
// if loan paid
|
||||
// check if loan is fully paid
|
||||
if (loanBalance <= 0 && !loanPaidOffMonth) {
|
||||
loanPaidOffMonth = `${currentYear}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
||||
loanPaidOffMonth = currentSimDate.format('YYYY-MM');
|
||||
}
|
||||
|
||||
// are we in college?
|
||||
// Are we still in college?
|
||||
let stillInCollege = false;
|
||||
if (inCollege) {
|
||||
if (graduationDateObj) {
|
||||
stillInCollege = date < graduationDateObj;
|
||||
stillInCollege = currentSimDate.isBefore(graduationDateObj, 'month');
|
||||
} else {
|
||||
stillInCollege = (elapsedMonths < totalAcademicMonths);
|
||||
stillInCollege = (monthIndex < totalAcademicMonths);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. tuition lumps
|
||||
/************************************************
|
||||
* 7.1 TUITION lumps
|
||||
************************************************/
|
||||
let tuitionCostThisMonth = 0;
|
||||
if (stillInCollege && lumpsPerYear > 0) {
|
||||
const academicYearIndex = Math.floor(elapsedMonths / 12);
|
||||
const monthInYear = elapsedMonths % 12;
|
||||
const academicYearIndex = Math.floor(monthIndex / 12);
|
||||
const monthInYear = monthIndex % 12;
|
||||
if (lumpsSchedule.includes(monthInYear) && academicYearIndex < finalProgramLength) {
|
||||
tuitionCostThisMonth = lumpAmount;
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Exiting college?
|
||||
const nowExitingCollege = wasInDeferral && !stillInCollege;
|
||||
|
||||
// 8. deferral lumps
|
||||
if (stillInCollege && loanDeferralUntilGraduation) {
|
||||
if (tuitionCostThisMonth > 0) {
|
||||
loanBalance += tuitionCostThisMonth;
|
||||
tuitionCostThisMonth = 0;
|
||||
}
|
||||
// If deferring tuition => add to loan, no direct expense
|
||||
if (stillInCollege && loanDeferralUntilGraduation && tuitionCostThisMonth > 0) {
|
||||
loanBalance += tuitionCostThisMonth;
|
||||
tuitionCostThisMonth = 0;
|
||||
}
|
||||
|
||||
// 9. Base monthly income
|
||||
let grossMonthlyIncome = 0;
|
||||
/************************************************
|
||||
* 7.2 BASE MONTHLY INCOME
|
||||
************************************************/
|
||||
let baseMonthlyIncome = 0;
|
||||
if (!stillInCollege) {
|
||||
grossMonthlyIncome = (expectedSalary > 0 ? expectedSalary : currentSalary) / 12;
|
||||
// user is out of college => expected or current
|
||||
baseMonthlyIncome = (expectedSalary || currentSalary) / 12;
|
||||
} else {
|
||||
grossMonthlyIncome = (currentSalary / 12) + (partTimeIncome / 12);
|
||||
// in college => might have partTimeIncome + current
|
||||
baseMonthlyIncome = (currentSalary / 12) + (partTimeIncome / 12);
|
||||
}
|
||||
|
||||
// Track extra subtracting impacts in a separate variable
|
||||
/************************************************
|
||||
* 7.3 MILESTONE IMPACTS
|
||||
************************************************/
|
||||
let extraImpactsThisMonth = 0;
|
||||
|
||||
// 9b. Apply milestone impacts
|
||||
milestoneImpacts.forEach((impact) => {
|
||||
const startOffset = impact.start_date
|
||||
? moment(impact.start_date).diff(moment(scenarioStart), 'months')
|
||||
: 0;
|
||||
const startDateClamped = moment(impact.start_date).startOf('month');
|
||||
let startOffset = startDateClamped.diff(scenarioStartClamped, 'months');
|
||||
if (startOffset < 0) startOffset = 0;
|
||||
|
||||
let endOffset = Infinity;
|
||||
if (impact.end_date && impact.end_date.trim() !== '') {
|
||||
endOffset = moment(impact.end_date).diff(moment(scenarioStart), 'months');
|
||||
const endDateClamped = moment(impact.end_date).startOf('month');
|
||||
endOffset = endDateClamped.diff(scenarioStartClamped, 'months');
|
||||
if (endOffset < 0) endOffset = 0;
|
||||
}
|
||||
|
||||
if (impact.impact_type === 'ONE_TIME') {
|
||||
if (elapsedMonths === startOffset) {
|
||||
if (monthIndex === startOffset) {
|
||||
if (impact.direction === 'add') {
|
||||
grossMonthlyIncome += impact.amount;
|
||||
baseMonthlyIncome += impact.amount;
|
||||
} else {
|
||||
extraImpactsThisMonth += impact.amount;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 'MONTHLY'
|
||||
if (elapsedMonths >= startOffset && elapsedMonths <= endOffset) {
|
||||
if (monthIndex >= startOffset && monthIndex <= endOffset) {
|
||||
if (impact.direction === 'add') {
|
||||
grossMonthlyIncome += impact.amount;
|
||||
baseMonthlyIncome += impact.amount;
|
||||
} else {
|
||||
extraImpactsThisMonth += impact.amount;
|
||||
}
|
||||
@ -326,70 +313,51 @@ export function simulateFinancialProjection(userProfile) {
|
||||
}
|
||||
});
|
||||
|
||||
// 10. Taxes
|
||||
if (!taxStateByYear[currentYear]) {
|
||||
taxStateByYear[currentYear] = {
|
||||
federalYtdGross: 0,
|
||||
federalYtdTaxSoFar: 0,
|
||||
stateYtdGross: 0,
|
||||
stateYtdTaxSoFar: 0
|
||||
};
|
||||
/************************************************
|
||||
* 7.4 CALCULATE TAXES (No YTD approach)
|
||||
************************************************/
|
||||
const monthlyFederalTax = calculateMonthlyFedTaxNoYTD(baseMonthlyIncome);
|
||||
const monthlyStateTax = calculateMonthlyStateTaxNoYTD(baseMonthlyIncome, stateCode);
|
||||
const combinedTax = monthlyFederalTax + monthlyStateTax;
|
||||
|
||||
// net after tax
|
||||
const netMonthlyIncome = baseMonthlyIncome - combinedTax;
|
||||
|
||||
// increment YTD gross & tax for reference
|
||||
fedYTDgross += baseMonthlyIncome;
|
||||
fedYTDtax += monthlyFederalTax;
|
||||
stateYTDgross += baseMonthlyIncome;
|
||||
stateYTDtax += monthlyStateTax;
|
||||
|
||||
/************************************************
|
||||
* 7.5 LOAN + EXPENSES
|
||||
************************************************/
|
||||
const nowExitingCollege = wasInDeferral && !stillInCollege;
|
||||
if (nowExitingCollege) {
|
||||
monthlyLoanPayment = calculateLoanPayment(loanBalance, interestRate, loanTerm);
|
||||
}
|
||||
|
||||
// accumulate YTD gross
|
||||
taxStateByYear[currentYear].federalYtdGross += grossMonthlyIncome;
|
||||
taxStateByYear[currentYear].stateYtdGross += grossMonthlyIncome;
|
||||
|
||||
// fed tax
|
||||
const newFedTaxTotal = calculateAnnualFederalTaxSingle(
|
||||
taxStateByYear[currentYear].federalYtdGross
|
||||
);
|
||||
const monthlyFederalTax = newFedTaxTotal - taxStateByYear[currentYear].federalYtdTaxSoFar;
|
||||
taxStateByYear[currentYear].federalYtdTaxSoFar = newFedTaxTotal;
|
||||
|
||||
// state tax
|
||||
const newStateTaxTotal = calculateAnnualStateTax(
|
||||
taxStateByYear[currentYear].stateYtdGross,
|
||||
stateCode
|
||||
);
|
||||
const monthlyStateTax = newStateTaxTotal - taxStateByYear[currentYear].stateYtdTaxSoFar;
|
||||
taxStateByYear[currentYear].stateYtdTaxSoFar = newStateTaxTotal;
|
||||
|
||||
const combinedTax = monthlyFederalTax + monthlyStateTax;
|
||||
const netMonthlyIncome = grossMonthlyIncome - combinedTax;
|
||||
|
||||
// 11. Expenses & loan
|
||||
let thisMonthLoanPayment = 0;
|
||||
// now include tuition lumps + any 'subtract' impacts
|
||||
let totalMonthlyExpenses = monthlyExpenses + monthlyDebtPayments + tuitionCostThisMonth + extraImpactsThisMonth;
|
||||
|
||||
// re-amortize after deferral ends
|
||||
if (nowExitingCollege) {
|
||||
monthlyLoanPayment = calculateLoanPayment(loanBalance, interestRate, 10);
|
||||
}
|
||||
|
||||
// if deferring
|
||||
if (stillInCollege && loanDeferralUntilGraduation) {
|
||||
// accumulate interest
|
||||
const interestForMonth = loanBalance * (interestRate / 100 / 12);
|
||||
loanBalance += interestForMonth;
|
||||
} else {
|
||||
// pay principal
|
||||
if (loanBalance > 0) {
|
||||
const interestForMonth = loanBalance * (interestRate / 100 / 12);
|
||||
const principalForMonth = Math.min(
|
||||
loanBalance,
|
||||
(monthlyLoanPayment + extraPayment) - interestForMonth
|
||||
);
|
||||
loanBalance -= principalForMonth;
|
||||
loanBalance = Math.max(loanBalance, 0);
|
||||
loanBalance = Math.max(loanBalance - principalForMonth, 0);
|
||||
|
||||
thisMonthLoanPayment = monthlyLoanPayment + extraPayment;
|
||||
totalMonthlyExpenses += thisMonthLoanPayment;
|
||||
totalMonthlyExpenses += (monthlyLoanPayment + extraPayment);
|
||||
}
|
||||
}
|
||||
|
||||
// leftover after mandatory expenses
|
||||
let leftover = netMonthlyIncome - totalMonthlyExpenses;
|
||||
if (leftover < 0) leftover = 0;
|
||||
|
||||
// baseline contributions
|
||||
const baselineContributions = monthlyRetirementContribution + monthlyEmergencyContribution;
|
||||
@ -402,59 +370,72 @@ export function simulateFinancialProjection(userProfile) {
|
||||
leftover -= baselineContributions;
|
||||
}
|
||||
|
||||
// shortfall check
|
||||
const totalWantedContributions = effectiveRetirementContribution + effectiveEmergencyContribution;
|
||||
const actualExpensesPaid = totalMonthlyExpenses + totalWantedContributions;
|
||||
const actualExpensesPaid = totalMonthlyExpenses + effectiveRetirementContribution + effectiveEmergencyContribution;
|
||||
let shortfall = actualExpensesPaid - netMonthlyIncome;
|
||||
|
||||
// cover shortfall with emergency
|
||||
if (shortfall > 0) {
|
||||
const canCover = Math.min(shortfall, currentEmergencySavings);
|
||||
currentEmergencySavings -= canCover;
|
||||
shortfall -= canCover;
|
||||
if (shortfall > 0) {
|
||||
// bankrupt scenario, end
|
||||
break;
|
||||
}
|
||||
// leftover -= shortfall; // if you want negative leftover
|
||||
}
|
||||
|
||||
// 13. Surplus
|
||||
// Surplus => leftover
|
||||
if (leftover > 0) {
|
||||
const totalPct = surplusEmergencyAllocation + surplusRetirementAllocation;
|
||||
const emergencyPortion = leftover * (surplusEmergencyAllocation / totalPct);
|
||||
const retirementPortion = leftover * (surplusRetirementAllocation / totalPct);
|
||||
const emergPortion = leftover * (surplusEmergencyAllocation / totalPct);
|
||||
const retPortion = leftover * (surplusRetirementAllocation / totalPct);
|
||||
|
||||
currentEmergencySavings += emergencyPortion;
|
||||
currentRetirementSavings += retirementPortion;
|
||||
currentEmergencySavings += emergPortion;
|
||||
currentRetirementSavings += retPortion;
|
||||
}
|
||||
|
||||
// netSavings for display
|
||||
const finalExpensesPaid = totalMonthlyExpenses + totalWantedContributions;
|
||||
const netSavings = netMonthlyIncome - finalExpensesPaid;
|
||||
// net savings
|
||||
const netSavings = netMonthlyIncome - actualExpensesPaid;
|
||||
|
||||
projectionData.push({
|
||||
month: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`,
|
||||
grossMonthlyIncome: Math.round(grossMonthlyIncome * 100) / 100,
|
||||
monthlyFederalTax: Math.round(monthlyFederalTax * 100) / 100,
|
||||
monthlyStateTax: Math.round(monthlyStateTax * 100) / 100,
|
||||
combinedTax: Math.round(combinedTax * 100) / 100,
|
||||
netMonthlyIncome: Math.round(netMonthlyIncome * 100) / 100,
|
||||
totalExpenses: Math.round(finalExpensesPaid * 100) / 100,
|
||||
effectiveRetirementContribution: Math.round(effectiveRetirementContribution * 100) / 100,
|
||||
effectiveEmergencyContribution: Math.round(effectiveEmergencyContribution * 100) / 100,
|
||||
netSavings: Math.round(netSavings * 100) / 100,
|
||||
emergencySavings: Math.round(currentEmergencySavings * 100) / 100,
|
||||
retirementSavings: Math.round(currentRetirementSavings * 100) / 100,
|
||||
loanBalance: Math.round(loanBalance * 100) / 100,
|
||||
loanPaymentThisMonth: Math.round(thisMonthLoanPayment * 100) / 100
|
||||
month: currentSimDate.format('YYYY-MM'),
|
||||
grossMonthlyIncome: +baseMonthlyIncome.toFixed(2),
|
||||
monthlyFederalTax: +monthlyFederalTax.toFixed(2),
|
||||
monthlyStateTax: +monthlyStateTax.toFixed(2),
|
||||
combinedTax: +combinedTax.toFixed(2),
|
||||
netMonthlyIncome: +netMonthlyIncome.toFixed(2),
|
||||
|
||||
totalExpenses: +actualExpensesPaid.toFixed(2),
|
||||
effectiveRetirementContribution: +effectiveRetirementContribution.toFixed(2),
|
||||
effectiveEmergencyContribution: +effectiveEmergencyContribution.toFixed(2),
|
||||
|
||||
netSavings: +netSavings.toFixed(2),
|
||||
emergencySavings: +currentEmergencySavings.toFixed(2),
|
||||
retirementSavings: +currentRetirementSavings.toFixed(2),
|
||||
loanBalance: +loanBalance.toFixed(2),
|
||||
|
||||
// actual loan payment
|
||||
loanPaymentThisMonth: +(monthlyLoanPayment + extraPayment).toFixed(2),
|
||||
|
||||
// YTD references
|
||||
fedYTDgross: +fedYTDgross.toFixed(2),
|
||||
fedYTDtax: +fedYTDtax.toFixed(2),
|
||||
stateYTDgross: +stateYTDgross.toFixed(2),
|
||||
stateYTDtax: +stateYTDtax.toFixed(2),
|
||||
});
|
||||
|
||||
// update deferral
|
||||
wasInDeferral = stillInCollege && loanDeferralUntilGraduation;
|
||||
}
|
||||
|
||||
return {
|
||||
projectionData,
|
||||
loanPaidOffMonth,
|
||||
finalEmergencySavings: Math.round(currentEmergencySavings * 100) / 100,
|
||||
finalRetirementSavings: Math.round(currentRetirementSavings * 100) / 100,
|
||||
finalLoanBalance: Math.round(loanBalance * 100) / 100
|
||||
finalEmergencySavings: +currentEmergencySavings.toFixed(2),
|
||||
finalRetirementSavings: +currentRetirementSavings.toFixed(2),
|
||||
finalLoanBalance: +loanBalance.toFixed(2),
|
||||
|
||||
// Final YTD totals
|
||||
fedYTDgross: +fedYTDgross.toFixed(2),
|
||||
fedYTDtax: +fedYTDtax.toFixed(2),
|
||||
stateYTDgross: +stateYTDgross.toFixed(2),
|
||||
stateYTDtax: +stateYTDtax.toFixed(2),
|
||||
};
|
||||
}
|
||||
|
BIN
user_profile.db
BIN
user_profile.db
Binary file not shown.
Loading…
Reference in New Issue
Block a user