diff --git a/backend/src/modules/board-planning/board-planning.service.ts b/backend/src/modules/board-planning/board-planning.service.ts index 61ed74e..a0c5c19 100644 --- a/backend/src/modules/board-planning/board-planning.service.ts +++ b/backend/src/modules/board-planning/board-planning.service.ts @@ -106,6 +106,37 @@ export class BoardPlanningService { async addInvestmentFromRecommendation(scenarioId: string, dto: any) { await this.getScenarioRow(scenarioId); + + // If the recommendation has components (e.g. CD ladder with multiple CDs), create one row per component + const components = dto.components as any[] | undefined; + if (components && Array.isArray(components) && components.length > 0) { + const results: any[] = []; + for (let i = 0; i < components.length; i++) { + const comp = components[i]; + const rows = await this.tenant.query( + `INSERT INTO scenario_investments + (scenario_id, source_recommendation_id, label, investment_type, fund_type, + principal, interest_rate, term_months, institution, notes, sort_order) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) + RETURNING *`, + [ + scenarioId, dto.sourceRecommendationId || null, + comp.label || `${dto.title || 'AI Recommendation'} - Part ${i + 1}`, + comp.investment_type || dto.investmentType || null, + dto.fundType || 'reserve', + comp.amount || 0, comp.rate || null, + comp.term_months || null, comp.bank_name || dto.bankName || null, + dto.rationale || dto.notes || null, + i, + ], + ); + results.push(rows[0]); + } + await this.invalidateProjectionCache(scenarioId); + return results; + } + + // Single investment (no components) const rows = await this.tenant.query( `INSERT INTO scenario_investments (scenario_id, source_recommendation_id, label, investment_type, fund_type, diff --git a/backend/src/modules/investment-planning/investment-planning.service.ts b/backend/src/modules/investment-planning/investment-planning.service.ts index 245c630..dfe6bd6 100644 --- a/backend/src/modules/investment-planning/investment-planning.service.ts +++ b/backend/src/modules/investment-planning/investment-planning.service.ts @@ -38,6 +38,15 @@ export interface MarketRate { fetched_at: string; } +export interface RecommendationComponent { + label: string; + amount: number; + term_months: number; + rate: number; + bank_name?: string; + investment_type?: string; +} + export interface Recommendation { type: 'cd_ladder' | 'new_investment' | 'reallocation' | 'maturity_action' | 'liquidity_warning' | 'general'; priority: 'high' | 'medium' | 'low'; @@ -50,6 +59,7 @@ export interface Recommendation { suggested_rate?: number; bank_name?: string; rationale: string; + components?: RecommendationComponent[]; } export interface AIResponse { @@ -904,13 +914,28 @@ Respond with ONLY valid JSON (no markdown, no code fences) matching this exact s "suggested_term": "12 months", "suggested_rate": 4.50, "bank_name": "Bank name from market rates (if applicable)", - "rationale": "Financial reasoning for why this makes sense" + "rationale": "Financial reasoning for why this makes sense", + "components": [ + { + "label": "Component label (e.g. '6-Month CD at Marcus')", + "amount": 6600.00, + "term_months": 6, + "rate": 4.05, + "bank_name": "Marcus", + "investment_type": "cd" + } + ] } ], "overall_assessment": "2-3 sentence overview of the HOA's current investment position and opportunities", "risk_notes": ["Array of risk items or concerns to flag for the board"] } +IMPORTANT ABOUT COMPONENTS: +- For cd_ladder recommendations, you MUST include a "components" array with each individual CD as a separate component. Each component should have its own label, amount, term_months, rate, and bank_name. The suggested_amount should be the total of all component amounts. +- For other multi-part strategies (e.g. splitting funds across multiple accounts), also include a "components" array. +- For simple single-investment recommendations, omit the "components" field entirely. + IMPORTANT: Provide 3-7 actionable recommendations. Prioritize high-priority items (liquidity risks, maturing investments) before optimization opportunities. Include specific dollar amounts wherever possible. When there are opportunities for better rates on existing positions, quantify the additional annual interest that could be earned.`; // Build the data context for the user prompt diff --git a/frontend/src/pages/investment-planning/InvestmentPlanningPage.tsx b/frontend/src/pages/investment-planning/InvestmentPlanningPage.tsx index 551eb97..b651876 100644 --- a/frontend/src/pages/investment-planning/InvestmentPlanningPage.tsx +++ b/frontend/src/pages/investment-planning/InvestmentPlanningPage.tsx @@ -85,6 +85,15 @@ interface MarketRatesResponse { high_yield_savings: MarketRate[]; } +interface RecommendationComponent { + label: string; + amount: number; + term_months: number; + rate: number; + bank_name?: string; + investment_type?: string; +} + interface Recommendation { type: string; priority: 'high' | 'medium' | 'low'; @@ -97,6 +106,7 @@ interface Recommendation { suggested_rate?: number; bank_name?: string; rationale: string; + components?: RecommendationComponent[]; } interface AIResponse { @@ -392,6 +402,7 @@ export function InvestmentPlanningPage() { termMonths: rec.suggested_term ? parseInt(rec.suggested_term) || null : null, bankName: rec.bank_name, rationale: rec.rationale, + components: rec.components || undefined, }); return scenarioId; }, @@ -421,6 +432,7 @@ export function InvestmentPlanningPage() { termMonths: rec.suggested_term ? parseInt(rec.suggested_term) || null : null, bankName: rec.bank_name, rationale: rec.rationale, + components: rec.components || undefined, }); return scenario.id; }, @@ -865,6 +877,16 @@ export function InvestmentPlanningPage() { {selectedRec.suggested_amount != null && ( Amount: {fmt(selectedRec.suggested_amount)} )} + {selectedRec.components && selectedRec.components.length > 0 && ( + + {selectedRec.components.length} investments will be created: + {selectedRec.components.map((c, i) => ( + + {c.label}: {fmt(c.amount)} @ {c.rate}% ({c.term_months} mo) + + ))} + + )} )}