faber-finances/app/composables/useCashFlow.ts

1062 lines
32 KiB
TypeScript

import { computed, ref } from "vue";
import type {
Transaction,
RevenueOpportunity,
CashFlowProjection,
RunwayScenarios,
CashPosition,
TransactionForecast,
CombinedCashFlow,
AffordabilityCheck,
} from "~/types/cashflow";
export const useCashFlow = () => {
const currentBalance = ref(0);
const accountBalances = ref<any>(null);
const transactions = ref<Transaction[]>([]);
const revenueOpportunities = ref<RevenueOpportunity[]>([]);
// Exchange rates for balance calculations - updated from Wise API
const exchangeRates = ref({
CAD: 1.0,
EUR: 1.45, // Will be updated from Wise API
USD: 1.35, // Fallback rate
GBP: 1.65 // Fallback rate
});
// All transactions are now personal
const filteredTransactions = computed(() => transactions.value);
// Balance tracking
const currentBalanceWithTransactions = computed(() => {
const transactionTotal = transactions.value
.reduce((sum, t) => sum + t.amount * t.probability, 0);
return currentBalance.value + transactionTotal;
});
// Starting balance (without transaction projections)
const startingBalance = computed(() => {
if (!accountBalances.value) {
return currentBalance.value;
}
const manual = accountBalances.value.manual || {};
const wise = accountBalances.value.wise || { jennie: [], henry: [] };
// Personal accounts: RBC + TD + Millennium + Wise
let totalBalance = 0;
totalBalance += manual.rbc_cad || 0;
totalBalance += manual.td_cad || 0;
totalBalance += (manual.millennium_eur || 0) * exchangeRates.value.EUR;
// Add Wise balances
[...(wise.jennie || []), ...(wise.henry || [])].forEach(balance => {
const rate = exchangeRates.value[balance.currency] || 1;
totalBalance += balance.value.value * rate;
});
return totalBalance;
});
// Fetch real-time exchange rates for balance calculations
const fetchExchangeRates = async () => {
try {
// Fetch EUR to CAD rate (most common conversion)
const eurResponse = await fetch('/api/wise/exchange-rates?source=EUR&target=CAD');
if (eurResponse.ok) {
const eurData = await eurResponse.json();
exchangeRates.value.EUR = eurData.rate;
}
} catch (error) {
console.error('Failed to update exchange rates:', error);
// Keep using fallback rates
}
};
// Load data from MongoDB on initialization
const loadData = async () => {
try {
// Load transactions
const transactionsResponse = await fetch("/api/transactions");
if (transactionsResponse.ok) {
const transactionsData = await transactionsResponse.json();
transactions.value = transactionsData.map((t: any) => ({
...t,
date: new Date(t.date),
endDate: t.endDate ? new Date(t.endDate) : undefined,
// Migration: set accountType to 'personal' if missing
accountType: t.accountType || "personal",
}));
}
// Load balance
const balanceResponse = await fetch("/api/balances");
if (balanceResponse.ok) {
const balanceData = await balanceResponse.json();
currentBalance.value = balanceData.currentBalance || 0;
accountBalances.value = balanceData.accountBalances || null;
// Fetch current exchange rates for accurate balance calculations
await fetchExchangeRates();
}
} catch (error) {
console.error("Failed to load data from MongoDB:", error);
// Try to migrate from localStorage as fallback
await tryMigrateFromLocalStorage();
}
};
// Attempt to migrate from localStorage if MongoDB fails
const tryMigrateFromLocalStorage = async () => {
if (typeof window !== "undefined") {
const savedBalance = localStorage.getItem("cashflow_balance");
const savedTransactions = localStorage.getItem("cashflow_transactions");
if (savedBalance || savedTransactions) {
try {
const migrationData = {
balance: savedBalance ? parseFloat(savedBalance) : 0,
transactions: savedTransactions
? JSON.parse(savedTransactions)
: [],
};
const response = await fetch("/api/migrate/localStorage", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(migrationData),
});
if (response.ok) {
console.log("Successfully migrated localStorage data to MongoDB");
// Clear localStorage after successful migration
localStorage.removeItem("cashflow_balance");
localStorage.removeItem("cashflow_transactions");
localStorage.removeItem("account_balances");
// Reload data from MongoDB
await loadData();
}
} catch (error) {
console.error("Migration failed:", error);
// Fall back to localStorage data
currentBalance.value = savedBalance ? parseFloat(savedBalance) : 0;
const parsedTransactions = savedTransactions
? JSON.parse(savedTransactions)
: [];
transactions.value = parsedTransactions.map((t: any) => ({
...t,
}));
}
}
}
};
// Initialize data loading
if (typeof window !== "undefined") {
loadData();
}
// Save to MongoDB helper
const saveToDatabase = async () => {
try {
// Save transactions
const response = await fetch("/api/transactions/bulk", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(transactions.value),
});
if (!response.ok) {
throw new Error("Failed to save transactions");
}
console.log("✅ Transactions saved to MongoDB");
} catch (error) {
console.error("Failed to save to MongoDB:", error);
// Fallback to localStorage if MongoDB fails
if (typeof window !== "undefined") {
localStorage.setItem(
"cashflow_balance",
currentBalance.value.toString()
);
localStorage.setItem(
"cashflow_transactions",
JSON.stringify(transactions.value)
);
}
}
};
// Update data methods
const updateTransactions = async (newTransactions: any[]) => {
console.log(
"🔄 updateTransactions called with:",
newTransactions.length,
"transactions"
);
transactions.value = newTransactions.map((t: any) => ({
...t,
type: t.type || (t.isRecurring ? "recurring" : "one-time"),
status: t.status || "committed",
frequency:
t.frequency ||
(t.isRecurring || t.type === "recurring" ? "monthly" : undefined),
probability: t.isConfirmed
? 1
: typeof t.probability === "number" &&
t.probability >= 0 &&
t.probability <= 1
? t.probability
: 0.5,
date: new Date(t.date),
endDate: t.endDate ? new Date(t.endDate) : undefined,
}));
console.log(
"✅ Transactions updated. Internal count:",
transactions.value.length
);
// Save to MongoDB
await saveToDatabase();
};
const updateRevenueOpportunities = (newOpportunities: any[]) => {
revenueOpportunities.value = newOpportunities.map((o: any) => ({
...o,
targetDate: new Date(o.targetDate),
}));
};
// Calculate current cash position including recurring projections
const currentCashPosition = computed((): CashPosition => {
const now = new Date();
const historicalInflow = filteredTransactions.value
.filter((t) => {
const transactionDate = new Date(t.date);
const isHistorical = t.status === "actual" || transactionDate <= now;
return t.amount > 0 && isHistorical;
})
.reduce((sum, t) => sum + t.amount * t.probability, 0);
const historicalOutflow = filteredTransactions.value
.filter((t) => {
const transactionDate = new Date(t.date);
const isHistorical = t.status === "actual" || transactionDate <= now;
return t.amount < 0 && isHistorical;
})
.reduce((sum, t) => sum + Math.abs(t.amount * t.probability), 0);
const currentMonthRecurringInflow = calculateRecurringInflowForAccount(now);
const currentMonthRecurringOutflow =
calculateRecurringOutflowForAccount(now);
return {
balance: currentBalanceWithTransactions.value,
totalInflow: historicalInflow,
totalOutflow: historicalOutflow,
netFlow: currentMonthRecurringInflow - currentMonthRecurringOutflow,
};
});
// Calculate monthly burn rate including recurring transactions
const monthlyBurnRate = computed(() => {
const now = new Date();
const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
const thisMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const historicalExpenses = filteredTransactions.value
.filter((t) => {
const transactionDate = new Date(t.date);
return (
transactionDate >= lastMonth &&
transactionDate < thisMonth &&
t.amount < 0 &&
t.type === "one-time"
);
})
.reduce((sum, t) => sum + Math.abs(t.amount * t.probability), 0);
const currentRecurringOutflow = calculateRecurringOutflowForAccount(now);
const futureRecurringOutflow = filteredTransactions.value
.filter((t) => {
if (t.type !== "recurring" || t.amount >= 0) return false;
const startDate = new Date(t.date);
const endDate = t.endDate
? new Date(t.endDate)
: new Date("2099-12-31");
return startDate > now && startDate <= endDate;
})
.reduce(
(sum, t) => sum + Math.abs(getMonthlyAmount(t) * t.probability),
0
);
return (
historicalExpenses + currentRecurringOutflow + futureRecurringOutflow
);
});
// Enhanced runway calculation with multiple scenarios
const calculateRunwayScenarios = (
programBudgets?: Record<string, any>
): RunwayScenarios => {
const totalContingency = programBudgets
? Object.values(programBudgets).reduce(
(sum, budget: any) => sum + (budget.contingency?.amount || 0),
0
)
: 0;
return {
conservative: calculateRunwayFromTransactions(
transactions.value.filter((t) => t.probability === 1.0),
startingBalance.value - totalContingency
),
realistic: calculateRunwayFromTransactions(
transactions.value,
startingBalance.value - totalContingency * 0.5
),
optimistic: calculateRunwayFromTransactions(
transactions.value.map((t) => ({ ...t, probability: 1.0 })),
startingBalance.value
),
cashOnly: (() => {
const dailyBurn = monthlyBurnRate.value / 30;
if (dailyBurn <= 0) return Infinity;
return Math.floor(startingBalance.value / dailyBurn);
})(),
};
};
// Calculate runway from transactions chronologically
const calculateRunwayFromTransactions = (
transactionList: Transaction[],
startingBalance: number
) => {
let runningBalance = startingBalance;
let dayCounter = 0;
const now = new Date();
// Use all transactions (personal only)
const filteredList = transactionList;
const futureTransactions = getFutureTransactionsWithOccurrences(
filteredList,
now
);
const sortedTransactions = futureTransactions.sort(
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
);
for (const transaction of sortedTransactions) {
const daysSinceStart = Math.floor(
(new Date(transaction.date).getTime() - now.getTime()) /
(1000 * 60 * 60 * 24)
);
const weightedAmount = transaction.amount * transaction.probability;
if (transaction.probability < 0.2) continue;
runningBalance += weightedAmount;
dayCounter = daysSinceStart;
if (runningBalance <= 0) {
const prevTransaction =
sortedTransactions[sortedTransactions.indexOf(transaction) - 1];
if (prevTransaction) {
const prevBalance = runningBalance - weightedAmount;
const interpolatedDay =
dayCounter - 1 + prevBalance / (prevBalance - runningBalance);
return Math.max(0, Math.round(interpolatedDay));
}
return Math.max(0, dayCounter);
}
}
if (runningBalance > 0) {
const monthlyBurn = monthlyBurnRate.value;
if (monthlyBurn <= 0) return Infinity;
const additionalMonths = runningBalance / monthlyBurn;
const additionalDays = additionalMonths * 30;
return Math.round(dayCounter + additionalDays);
}
return dayCounter;
};
// Get future transactions including recurring occurrences
const getFutureTransactionsWithOccurrences = (
transactionList: Transaction[],
fromDate: Date
) => {
const futureTransactions: Transaction[] = [];
const projectionPeriod = 365;
for (const transaction of transactionList) {
if (transaction.isTest) continue;
if (transaction.type === "one-time") {
const transactionDate = new Date(transaction.date);
if (transactionDate > fromDate) {
futureTransactions.push(transaction);
}
} else if (transaction.type === "recurring") {
const startDate = new Date(transaction.date);
const endDate = transaction.endDate
? new Date(transaction.endDate)
: new Date("2099-12-31");
if (endDate > fromDate) {
const occurrences = generateRecurringOccurrences(
transaction,
fromDate,
projectionPeriod
);
futureTransactions.push(...occurrences);
}
}
}
for (const opportunity of revenueOpportunities.value) {
const opportunityDate = new Date(opportunity.targetDate);
if (opportunityDate > fromDate) {
futureTransactions.push({
id: opportunity._id || opportunity.id,
date: opportunityDate,
description: `Pipeline: ${opportunity.source}`,
amount: opportunity.amount,
category: "Income",
type: "one-time",
probability: opportunity.probability,
isConfirmed: false,
status: "projected",
notes: `Stage: ${opportunity.stage}`,
});
}
}
return futureTransactions;
};
// Generate recurring transaction occurrences
const generateRecurringOccurrences = (
transaction: Transaction,
fromDate: Date,
days: number
) => {
const occurrences: Transaction[] = [];
const endDate = new Date(fromDate);
endDate.setDate(endDate.getDate() + days);
const transactionEndDate = transaction.endDate
? new Date(transaction.endDate)
: endDate;
const actualEndDate =
transactionEndDate < endDate ? transactionEndDate : endDate;
let currentDate = new Date(
Math.max(fromDate.getTime(), new Date(transaction.date).getTime())
);
while (currentDate <= actualEndDate) {
const frequency = transaction.frequency || "monthly";
let nextDate: Date;
switch (frequency) {
case "weekly":
nextDate = new Date(currentDate);
nextDate.setDate(nextDate.getDate() + 7);
break;
case "biweekly":
nextDate = new Date(currentDate);
nextDate.setDate(nextDate.getDate() + 14);
break;
case "monthly":
nextDate = new Date(currentDate);
nextDate.setMonth(nextDate.getMonth() + 1);
break;
case "quarterly":
nextDate = new Date(currentDate);
nextDate.setMonth(nextDate.getMonth() + 3);
break;
case "custom":
nextDate = new Date(currentDate);
nextDate.setMonth(
nextDate.getMonth() + (transaction.customMonths || 1)
);
break;
default:
nextDate = new Date(currentDate);
nextDate.setMonth(nextDate.getMonth() + 1);
}
if (currentDate > fromDate && currentDate <= actualEndDate) {
occurrences.push({
...transaction,
date: new Date(currentDate),
id: `${transaction.id}-${currentDate.getTime()}`,
});
}
currentDate = nextDate;
if (occurrences.length > 1000) break;
}
return occurrences;
};
// Calculate runway in days (now uses realistic scenario)
const runwayDays = computed(() => {
const scenarios = calculateRunwayScenarios();
return scenarios.realistic;
});
// Calculate risk level
const riskLevel = computed(() => {
const runway = runwayDays.value;
const balance = currentBalance.value;
if (runway < 30 || balance < 25000) return "critical";
if (runway < 60 || balance < 50000) return "high";
if (runway < 90 || balance < 75000) return "medium";
return "low";
});
// Calculate available cash (unrestricted funds only)
const availableCash = computed(() => {
return currentBalance.value;
});
// Calculate restricted cash
const restrictedCash = computed(() => {
return 0;
});
// Calculate total cash
const totalCash = computed(() => {
return availableCash.value + restrictedCash.value;
});
// Calculate available runway based on the standard runway calculation
const availableRunwayDays = computed(() => {
return runwayDays.value;
});
// Available cash risk level
const availableRiskLevel = computed(() => {
const runway = availableRunwayDays.value;
const balance = availableCash.value;
if (runway < 14 || balance < 10000) return "critical";
if (runway < 28 || balance < 25000) return "high";
if (runway < 42 || balance < 40000) return "medium";
return "low";
});
// Generate cash flow projections
const generateProjections = (
months: number = 12,
timeContext?: { startDate?: Date; endDate?: Date }
) => {
const projections: CashFlowProjection[] = [];
let runningBalance = currentBalance.value;
if (timeContext && timeContext.startDate && timeContext.endDate) {
const startDate = new Date(timeContext.startDate);
const endDate = new Date(timeContext.endDate);
const monthsDiff = Math.round(
(endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24 * 30)
);
months = Math.max(1, Math.min(monthsDiff, 24));
}
for (let i = 0; i < months; i++) {
const projectionDate = new Date();
projectionDate.setMonth(projectionDate.getMonth() + i);
const monthlyInflow = calculateRecurringInflow(projectionDate);
const monthlyOutflow = calculateRecurringOutflow(projectionDate);
const oneTimeTransactions =
getOneTimeTransactionsForMonth(projectionDate);
let oneTimeInflow = 0;
let oneTimeOutflow = 0;
oneTimeTransactions.forEach((t) => {
const amount = t.amount * t.probability;
if (amount > 0) {
if (t.status === "actual" || t.status === "committed") {
oneTimeInflow += amount;
}
} else {
oneTimeOutflow += Math.abs(amount);
}
});
const opportunityInflow = 0;
const totalInflow = monthlyInflow + oneTimeInflow + opportunityInflow;
const totalOutflow = monthlyOutflow + oneTimeOutflow;
const netFlow = totalInflow - totalOutflow;
runningBalance += netFlow;
projections.push({
date: new Date(projectionDate),
balance: runningBalance,
inflow: totalInflow,
outflow: totalOutflow,
netFlow: netFlow,
projectedBalance: runningBalance,
});
}
return projections;
};
// Generate weekly cash flow projections
const generateWeeklyProjections = (weeks: number = 13) => {
const projections: CashFlowProjection[] = [];
let runningBalance = currentBalance.value;
for (let i = 0; i < weeks; i++) {
const weekStart = new Date();
weekStart.setDate(weekStart.getDate() + i * 7);
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekEnd.getDate() + 6);
weekEnd.setHours(23, 59, 59, 999);
let weeklyInflow = 0;
let weeklyOutflow = 0;
transactions.value
.filter((t) => !t.isTest)
.forEach((transaction) => {
const amount = transaction.amount * transaction.probability;
if (transaction.type === "one-time") {
const txnDate = new Date(transaction.date);
if (txnDate >= weekStart && txnDate <= weekEnd) {
if (amount > 0) {
if (
transaction.status === "actual" ||
transaction.status === "committed"
) {
weeklyInflow += amount;
}
} else {
weeklyOutflow += Math.abs(amount);
}
}
} else if (transaction.type === "recurring") {
const startDate = new Date(transaction.date);
const endDate = transaction.endDate
? new Date(transaction.endDate)
: new Date("2099-12-31");
if (weekStart <= endDate && weekEnd >= startDate) {
const weeklyAmount = getWeeklyAmountForTransaction(transaction);
if (weeklyAmount > 0) {
weeklyInflow += weeklyAmount;
} else {
weeklyOutflow += Math.abs(weeklyAmount);
}
}
}
});
revenueOpportunities.value.forEach((opportunity) => {
const oppDate = new Date(opportunity.targetDate);
if (oppDate >= weekStart && oppDate <= weekEnd) {
weeklyInflow += opportunity.amount * opportunity.probability;
}
});
const netFlow = weeklyInflow - weeklyOutflow;
runningBalance += netFlow;
projections.push({
date: new Date(weekStart),
balance: runningBalance,
inflow: weeklyInflow,
outflow: weeklyOutflow,
netFlow,
projectedBalance: runningBalance,
});
}
return projections;
};
// Helper function to calculate weekly amount for recurring transactions
const getWeeklyAmountForTransaction = (transaction: Transaction): number => {
if (transaction.type !== "recurring") return 0;
const frequency = transaction.frequency || "monthly";
const baseAmount = transaction.amount * transaction.probability;
switch (frequency) {
case "weekly":
return baseAmount;
case "biweekly":
return baseAmount / 2;
case "monthly":
return baseAmount / 4.33;
case "quarterly":
return baseAmount / 13;
case "custom":
if (transaction.customMonths) {
return baseAmount / (transaction.customMonths * 4.33);
}
return baseAmount / 4.33;
default:
return baseAmount / 4.33;
}
};
// Helper functions
const calculateRecurringInflow = (date: Date): number => {
return transactions.value
.filter(
(t) =>
t.type === "recurring" &&
t.amount > 0 &&
(t.status === "actual" || t.status === "committed") &&
isTransactionActiveInMonth(t, date)
)
.reduce((sum, t) => sum + getMonthlyAmount(t) * t.probability, 0);
};
const calculateRecurringOutflow = (date: Date): number => {
return transactions.value
.filter(
(t) =>
t.type === "recurring" &&
t.amount < 0 &&
(t.status === "actual" || t.status === "committed") &&
isTransactionActiveInMonth(t, date)
)
.reduce(
(sum, t) => sum + Math.abs(getMonthlyAmount(t) * t.probability),
0
);
};
// Account-specific helper functions
const calculateRecurringInflowForAccount = (date: Date): number => {
return filteredTransactions.value
.filter(
(t) =>
t.type === "recurring" &&
t.amount > 0 &&
(t.status === "actual" || t.status === "committed") &&
isTransactionActiveInMonth(t, date)
)
.reduce((sum, t) => sum + getMonthlyAmount(t) * t.probability, 0);
};
const calculateRecurringOutflowForAccount = (date: Date): number => {
return filteredTransactions.value
.filter(
(t) =>
t.type === "recurring" &&
t.amount < 0 &&
(t.status === "actual" || t.status === "committed") &&
isTransactionActiveInMonth(t, date)
)
.reduce(
(sum, t) => sum + Math.abs(getMonthlyAmount(t) * t.probability),
0
);
};
const getOneTimeTransactionsForMonth = (date: Date): Transaction[] => {
return transactions.value.filter((t) => {
if (t.type !== "one-time") return false;
const transactionDate = new Date(t.date);
return (
transactionDate.getMonth() === date.getMonth() &&
transactionDate.getFullYear() === date.getFullYear()
);
});
};
const isTransactionActiveInMonth = (
transaction: Transaction,
date: Date
): boolean => {
const startDate = new Date(transaction.date);
const endDate = transaction.endDate
? new Date(transaction.endDate)
: new Date("2099-12-31");
const monthStart = new Date(date.getFullYear(), date.getMonth(), 1);
const monthEnd = new Date(
date.getFullYear(),
date.getMonth() + 1,
0,
23,
59,
59,
999
);
return monthEnd >= startDate && monthStart <= endDate;
};
const getMonthlyAmount = (transaction: Transaction): number => {
if (transaction.type !== "recurring") return 0;
const frequencyMultiplier = {
weekly: 4.33,
biweekly: 2.17,
monthly: 1,
quarterly: 0.33,
custom: transaction.customMonths ? 12 / transaction.customMonths : 1,
};
return (
transaction.amount *
(frequencyMultiplier[transaction.frequency || "monthly"] || 1)
);
};
const setCurrentBalance = async (balance: number) => {
currentBalance.value = balance;
try {
// Get existing balance data first to preserve account balances
const existingResponse = await fetch("/api/balances");
let existingData = null;
if (existingResponse.ok) {
existingData = await existingResponse.json();
}
// Only update the total balance, preserve existing account balances
const response = await fetch("/api/balances", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
currentBalance: balance,
accountBalances: existingData?.accountBalances || {
manual: {
rbc_cad: 0,
td_cad: 0,
millennium_eur: 0
},
wise: { jennie: [], henry: [] },
},
}),
});
if (!response.ok) {
throw new Error("Failed to save balance");
}
// Update local accountBalances
accountBalances.value = existingData?.accountBalances || null;
console.log("✅ Balance saved to MongoDB");
} catch (error) {
console.error("Failed to save balance to MongoDB:", error);
// Fallback to localStorage
if (typeof window !== "undefined") {
localStorage.setItem("cashflow_balance", balance.toString());
}
}
};
// Affordability checker functions
const canAfford = (
amount: number,
targetDate?: Date,
fundType: "restricted" | "unrestricted" = "unrestricted"
): AffordabilityCheck => {
const testDate = targetDate || new Date();
if (fundType === "restricted") {
const availableRestricted = restrictedCash.value;
if (Math.abs(amount) > availableRestricted) {
return {
canAfford: false,
reason: "Insufficient restricted funds",
availableAmount: availableRestricted,
shortfall: Math.abs(amount) - availableRestricted,
};
}
}
const projectedAvailableCash = getAvailableCashAtDate(testDate);
const newBalance = projectedAvailableCash + amount;
const weeklyBurn = getWeeklyBurnRate();
const newRunwayWeeks = weeklyBurn > 0 ? newBalance / weeklyBurn : Infinity;
return {
canAfford: newRunwayWeeks >= 4,
currentAvailable: projectedAvailableCash,
newBalance,
currentRunwayWeeks:
weeklyBurn > 0 ? projectedAvailableCash / weeklyBurn : Infinity,
newRunwayWeeks,
riskLevel: getRiskLevelForRunway(newRunwayWeeks),
recommendation: getAffordabilityRecommendation(newRunwayWeeks, amount),
};
};
const getAvailableCashAtDate = (date: Date): number => {
let projectedBalance = availableCash.value;
const now = new Date();
const futureTxns = transactions.value.filter((t) => {
if (t.isTest) return false;
if (t.fundType === "restricted") return false;
const txnDate = new Date(t.date);
return txnDate > now && txnDate <= date;
});
futureTxns.forEach((t) => {
projectedBalance += t.amount * t.probability;
});
return projectedBalance;
};
const getWeeklyBurnRate = (): number => {
return monthlyBurnRate.value / 4.33;
};
const getRiskLevelForRunway = (runwayWeeks: number): string => {
if (runwayWeeks < 2) return "critical";
if (runwayWeeks < 4) return "high";
if (runwayWeeks < 6) return "medium";
return "low";
};
const getAffordabilityRecommendation = (
runwayWeeks: number,
amount: number
): string => {
if (runwayWeeks >= 6) return "Safe to proceed";
if (runwayWeeks >= 4) return "Proceed with caution - monitor cash flow";
if (runwayWeeks >= 2)
return "High risk - consider delaying or reducing amount";
return "Do not proceed - insufficient runway";
};
// Test transaction management
const createTestTransaction = (
description: string,
amount: number,
date: Date,
program?: string,
fundType: "restricted" | "unrestricted" = "unrestricted"
): Transaction => {
const testTxn: Transaction = {
id: `test-${Date.now()}`,
date,
description: `[TEST] ${description}`,
amount,
category: "Expense: Want",
type: "one-time",
program,
status: "projected",
fundType,
probability: 1.0,
isConfirmed: false,
isTest: true,
notes: "Test transaction for affordability checking",
};
transactions.value.push(testTxn);
return testTxn;
};
const removeTestTransaction = (transactionId: string) => {
transactions.value = transactions.value.filter(
(t) => !(t.isTest && t.id === transactionId)
);
};
const removeAllTestTransactions = () => {
transactions.value = transactions.value.filter((t) => !t.isTest);
};
// Delete transaction method
const deleteTransaction = async (transactionId: string) => {
try {
// Call API to delete from database
const response = await fetch(`/api/transactions/${transactionId}`, {
method: "DELETE",
});
if (!response.ok) {
throw new Error("Failed to delete transaction from database");
}
// Remove from local state
transactions.value = transactions.value.filter(
(t) => t.id !== transactionId
);
console.log("✅ Transaction deleted successfully");
return { success: true };
} catch (error) {
console.error("Failed to delete transaction:", error);
throw error;
}
};
return {
// State
currentBalance,
transactions,
revenueOpportunities,
// Computed - Original
currentCashPosition,
monthlyBurnRate,
runwayDays,
riskLevel,
// Computed - Fund Tracking
availableCash,
restrictedCash,
totalCash,
availableRunwayDays,
availableRiskLevel,
// Computed
filteredTransactions,
currentBalanceWithTransactions,
startingBalance,
startingBalanceForActive: startingBalance, // Alias for compatibility
// Methods - Original
generateProjections,
generateWeeklyProjections,
updateTransactions,
updateRevenueOpportunities,
setCurrentBalance,
// Methods - Affordability & Testing
canAfford,
getAvailableCashAtDate,
createTestTransaction,
removeTestTransaction,
removeAllTestTransactions,
deleteTransaction,
// Enhanced runway calculations
calculateRunwayScenarios,
};
};