Skip to main content

Goal Page Business Logic

This document describes the business logic, formulas, and component interactions for the Goals feature in WealthVN.

Table of Contents​

  1. Overview
  2. User Flows
  3. Core Data Types
  4. Goals Page (goals-page.tsx)
  5. GoalItem Component
  6. Goal Allocations Table
  7. Goal Form (Add/Edit)
  8. Key Formulas
  9. Business Rules

Overview​

The Goals feature allows users to:

  • Create financial goals with target amounts and timelines
  • Allocate portions of their investment accounts toward specific goals
  • Track progress through contributed value calculations
  • Monitor if they're on/off track based on projected vs actual values

User Flows​

1. Create New Goal​

2. Manage Allocations​

This flow describes how users assign account funds to goals.

3. Track Progress (Auto)​

How the system determines if a goal is "On Track".

File Structure​

ComponentLocation
Goals Pagesrc/pages/goals/goals-page.tsx
Goal Details Pagesrc/pages/goals/goal-details-page.tsx
GoalItemsrc/pages/goals/components/goal-item.tsx
GoalAllocationssrc/pages/goals/components/goal-allocations.tsx
GoalFormsrc/pages/goals/components/goal-form.tsx
useGoalProgress Hooksrc/pages/goals/use-goal-progress.ts
Goal Utilssrc/pages/goals/lib/goal-utils.ts
Goal Commandssrc/commands/goal.ts

Core Data Types​

Goal Interface​

interface Goal {
id: string;
title: string;
description?: string;
targetAmount: number; // The target value to reach
targetReturnRate?: number; // Expected annual return rate (0-100%)
isAchieved?: boolean; // Manually marked as complete
dueDate?: string; // ISO date string - when goal should be achieved
monthlyInvestment?: number; // Monthly DCA contribution amount
startDate?: string; // ISO date string - when goal tracking begins
allocations?: GoalAllocation[];
}

GoalAllocation Interface​

interface GoalAllocation {
id: string;
goalId: string;
accountId: string;
initialContribution: number; // Locked initial amount at allocation start
allocatedPercent: number; // Percentage of account GROWTH attributed to goal (0-100)
allocationDate?: string; // YYYY-MM-DD format - when allocation becomes active
}

GoalProgress Interface (Internal)​

interface GoalProgress {
goalId: string;
currentValue: number; // Current contributed value
targetAmount: number; // Target from goal
progress: number; // Percentage (0-100)
expectedProgress: number; // Timeline-based expectation
isOnTrack: boolean; // currentValue >= projectedValue
projectedValue: number; // Expected value at today based on plan
}

Goals Page​

File: src/pages/goals/goals-page.tsx

Data Flow​

  1. Fetch Goals - Query all goals from backend
  2. Fetch Allocations - Query all goal allocations
  3. Fetch Latest Valuations - Get current account values (NOT account.balance)
  4. Calculate Progress - Use useGoalProgress hook to compute each goal's progress

Key Implementation​

// Get latest account valuations (source of truth for account values)
const { latestValuations } = useLatestValuations(accountIds);

// Build current account values map
const currentAccountValuesFromValuations = new Map(
(latestValuations || []).map((val) => [val.accountId, val.totalValue]),
);

// Calculate progress for each goal
const { getGoalProgress } = useGoalProgress(goals);

Display Elements​

  • Goal Cards Grid - 3-column responsive grid of GoalItem components
  • Allocations Table - Read-only matrix showing all goal-account allocations
  • Empty State - Placeholder when no goals exist

GoalItem Component​

File: src/pages/goals/components/goal-item.tsx

Props​

interface GoalItemProps {
goal: Goal;
currentValue?: number; // Calculated contributed value from allocations
progress?: number; // Percentage (0-100)
isOnTrack?: boolean; // Actual >= Projected
allocations?: GoalAllocation[]; // Allocations for this specific goal
totalAccountCount?: number; // Total number of accounts in portfolio
onEdit: (goal: Goal) => void;
onDelete: (goal: Goal) => void;
}

Display Elements​

ElementSourceDescription
Titlegoal.titleGoal name
Status BadgeComputedCompleted/Not Started/Overdue/Ongoing
Allocation BadgeComputedAverage allocation percentage across accounts
Current ValuecurrentValue propFormatted currency value
Target Valuegoal.targetAmountFormatted currency value
Progress Barprogress propVisual percentage with color based on track status
Track StatusisOnTrack prop"On Track" / "Off Track" / "Done"

Allocation Badge Calculation​

The allocation badge shows the average allocation percentage across all accounts:

// Number of accounts allocated to this goal
const allocationCount = allocations.length;

// Sum of all allocation percentages for this goal
const totalAllocationPercent = allocations.reduce(
(sum, a) => sum + a.allocatedPercent,
0,
);

// Average: (sum of percentages) / (total accounts * 100) * 100
// Example: Goal has 50% from account1 + 50% from account2, with 2 total accounts
// (50 + 50) / (2 * 100) * 100 = 50%
const averageAllocationPercent =
totalAccountCount > 0
? (totalAllocationPercent / (totalAccountCount * 100)) * 100
: 0;

Interpretation: If a goal has allocations totaling 100% across 2 accounts out of 4 total accounts, the average is 25% (100 / 400 * 100).

Status Logic​

const getGoalStatus = () => {
// Priority 1: Check if achieved
if (goal.isAchieved) return "Completed";

const today = new Date();
const startDate = goal.startDate ? new Date(goal.startDate) : null;
const dueDate = goal.dueDate ? new Date(goal.dueDate) : null;

// Priority 2: Check if not started yet
if (startDate && startDate > today) return "Not Started";

// Priority 3: Check if past due date
if (dueDate && dueDate < today) return "Overdue";

// Default: Goal is active
return "Ongoing";
};

Track Status Logic​

const getTrackStatus = () => {
// Achieved goals show "Done"
if (goal.isAchieved) return { text: "Done", color: "success" };

// Goals not started yet show no status
if (startDate && startDate > today) return { text: "", color: "muted" };

// Active goals compare actual vs projected
if (isOnTrack) return { text: "On Track", color: "on-track-green" };

return { text: "Off Track", color: "off-track-amber" };
};

Progress Bar Color​

  • Green (bg-primary) - Goal is on track OR achieved
  • Amber (bg-amber-500) - Goal is off track

Goal Allocations Table​

File: src/pages/goals/components/goal-allocations.tsx

Structure​

A matrix table with:

  • Rows: One per goal
  • Columns: One per account + goal name column
  • Cells: Allocation percentage input (or display in read-only mode)

Props​

interface GoalsAllocationsProps {
goals: Goal[];
accounts: Account[];
existingAllocations?: GoalAllocation[];
allAllocations?: GoalAllocation[]; // All allocations including other goals
onSubmit: (allocations: GoalAllocation[]) => void;
readOnly?: boolean;
showRemaining?: boolean;
currentAccountValues?: Map<string, number>; // For displaying remaining value
currency?: string;
}

Unallocated Row Calculation​

The table shows an "Unallocated" row at the top:

// Remaining percentage for each account
const remainingPercent = Math.max(0, 100 - totalAllocations[account.id]);

// Remaining value in currency
// Sum of initial contributions already allocated
const totalAllocated = allocations.reduce((sum, alloc) => {
return alloc.accountId === account.id
? sum + (alloc.initialContribution || 0)
: sum;
}, 0);

// Remaining = Current Account Value - Total Allocated
const remainingValue = Math.max(0, currentValue - totalAllocated);

Table Display Per Goal Row​

Each goal row shows two sub-rows:

  1. Percentage Row - Input field for allocatedPercent
  2. Initial Contribution Row - Display of initialContribution amount

Validation​

  • Total allocations per account cannot exceed 100%
  • Submit is blocked if any account exceeds 100%

Goal Form​

File: src/pages/goals/components/goal-form.tsx

Form Fields​

FieldTypeRequiredDescription
titleTextYesGoal name
descriptionTextareaNoGoal description
targetAmountMoneyYesTarget value to achieve
startDateDateNoWhen goal tracking begins
dueDateDateNoTarget completion date
targetReturnRateNumber (0-100)NoExpected annual return %
monthlyInvestmentMoneyNoMonthly DCA amount (auto-calculated)
isAchievedSwitchNoMark goal as complete (edit mode only)

Auto-Calculate Monthly Investment​

When targetAmount, targetReturnRate, startDate, and dueDate are all provided, the monthly investment is auto-calculated:

// Calculate months between dates
const monthsDiff =
(end.getFullYear() - start.getFullYear()) * 12 +
(end.getMonth() - start.getMonth());

// Monthly interest rate
const monthlyRate = targetReturnRate / 100 / 12;

let monthlyInvestment: number;

if (monthlyRate === 0) {
// Simple division (no compound interest)
monthlyInvestment = targetAmount / monthsDiff;
} else {
// PMT formula: Target / [((1 + r)^n - 1) / r]
const compoundFactor = Math.pow(1 + monthlyRate, monthsDiff);
monthlyInvestment = targetAmount / ((compoundFactor - 1) / monthlyRate);
}

Formula (PMT for Future Value of Annuity):

PMT = FV / [((1 + r)^n - 1) / r]

Where:
- FV = targetAmount (future value target)
- r = monthlyRate (targetReturnRate / 100 / 12)
- n = monthsDiff (number of months)

Key Formulas​

1. Current Value (Contributed Value)​

File: src/pages/goals/use-goal-progress.ts

The current value of a goal is the sum of contributed values from all allocations:

Current Value = Σ (Initial Contribution + Allocated Growth)

Where for each allocation:
Allocated Growth = (Current Account Value - Account Value at Start) × (allocatedPercent / 100)

Code:

// For each allocation
const accountGrowth = currentAccountValue - startAccountValue;
const allocatedGrowth = accountGrowth * (allocatedPercent / 100);
const allocatedValue = initialContribution + allocatedGrowth;

// Sum all allocations
currentValue += allocatedValue;

Example:

  • Initial Contribution: $10,000
  • Account value at allocation start: $50,000
  • Current account value: $60,000
  • Allocated percent: 50%
  • Account Growth: $60,000 - $50,000 = $10,000
  • Allocated Growth: $10,000 × 50% = $5,000
  • Contributed Value: $10,000 + $5,000 = $15,000

2. Progress Percentage​

const progress =
goal.targetAmount > 0
? Math.min((currentValue / goal.targetAmount) * 100, 100)
: 0;

Formula:

Progress = min((Current Value / Target Amount) × 100, 100)

Progress is capped at 100%.

3. Projected Value (for On-Track Determination)​

Uses compound interest formula with regular contributions:

FV = PMT × [((1 + r)^n - 1) / r]

Where:
- FV = Projected Future Value (Growth from contributions only)
- PMT = monthlyInvestment
- r = monthlyRate (annualReturnRate / 100 / 12)
- n = monthsFromStart (months from goal start to today)

Code:

function calculateProjectedValue(
_startValue: number, // Unused: Initial contributions are excluded from projection logic
monthlyInvestment: number,
annualReturnRate: number,
monthsFromStart: number,
): number {
if (monthsFromStart <= 0) return 0;

const monthlyRate = annualReturnRate / 100 / 12;

if (monthlyRate === 0) {
// No compound interest
return monthlyInvestment * monthsFromStart;
}

const compoundFactor = Math.pow(1 + monthlyRate, monthsFromStart);
const futureContributions =
monthlyInvestment * ((compoundFactor - 1) / monthlyRate);

return futureContributions;
}

Note: The actual implementation uses calculateProjectedValueByDate for higher precision, but the logic remains the same regarding the exclusion of initial principal growth in the "Projected" line.

4. On-Track Determination​

File: src/pages/goals/lib/goal-utils.ts

export function isGoalOnTrack(
currentValue: number,
projectedValue: number,
): boolean {
return currentValue >= projectedValue;
}

Rule: A goal is "on track" if the actual current value meets or exceeds the projected value at today's date.

5. Months From Start Calculation​

const goalStartDate = parseISO(goal.startDate);
const today = new Date();
const yearDiff = today.getFullYear() - goalStartDate.getFullYear();
const monthDiff = today.getMonth() - goalStartDate.getMonth();
const daysDiff = today.getDate() - goalStartDate.getDate();

// Fractional months for precision
const monthsFromStart = yearDiff * 12 + monthDiff + daysDiff / 30;

Business Rules​

Allocation Rules​

  1. 100% Cap Per Account - Sum of all allocatedPercent for a single account across all goals cannot exceed 100%

  2. Initial Contribution Cap - Cannot exceed the unallocated balance at the allocation date

  3. Growth Attribution - Account growth (positive or negative) is split proportionally among goals based on their allocatedPercent

  4. Negative Growth - Losses reduce goal value (allocated growth can be negative)

  5. Allocation Date Filtering - Allocations with a future allocationDate are excluded from current value calculations

Goal Status Rules​

ConditionStatus
isAchieved === trueCompleted
startDate > todayNot Started
dueDate < today AND not achievedOverdue
OtherwiseOngoing

Track Status Rules​

ConditionStatus
isAchieved === trueDone
startDate > today(no status shown)
currentValue >= projectedValueOn Track
currentValue < projectedValueOff Track

Data Source Rules​

  1. Account Values - Always use latestValuations from the valuations query, NOT account.balance which may be stale

  2. Historical Values - For growth calculations, fetch historical valuations at the allocation/goal start date

  3. Currency - All values displayed in user's base currency from settings