trading-platform/docs/02-definicion-modulos/OQI-008-portfolio-manager/especificaciones/ET-PFM-011-goals-system.md
Adrian Flores Cortes 618e3220bd [F1-F3] feat: Complete entity types, stores, and documentation
FASE 1 - DDL-Backend Coherence (continued):
- market-data.types.ts: Updated TickerRow, added Ohlcv5mRow, Ohlcv15mRow, OhlcvStagingRow
- llm.types.ts: Updated UserPreferences, UserMemory, Embedding + 3 Row types
- financial.types.ts: +6 types (Invoice, WalletAuditLog, etc.)
- entity.types.ts (trading): +5 types (Symbol, TradingBot, etc.)

FASE 2 - Backend-Frontend Coherence (continued):
- llmStore.ts: New Zustand store with session lifecycle management
- riskStore.ts: New Zustand store for risk assessment
- risk.service.ts: New service with 8 functions
- currency.service.ts: New service with 5 functions

FASE 3 - Documentation:
- OQI-007: Updated to 100% (7 ET, 11 US, 6 RF)
- OQI-008: Added ET-PFM-010-architecture.md, ET-PFM-011-goals-system.md
- Updated all _MAP.md and README.md indexes

Build validation: Backend tsc PASSED, Frontend Vite PASSED

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 22:39:10 -06:00

875 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
id: "ET-PFM-011"
title: "Sistema de Goals (Metas de Inversion)"
type: "Technical Specification"
status: "Done"
priority: "Alta"
epic: "OQI-008"
project: "trading-platform"
version: "1.0.0"
created_date: "2026-01-28"
updated_date: "2026-01-28"
---
# ET-PFM-011: Sistema de Goals (Metas de Inversion)
**Epica:** OQI-008 - Portfolio Manager
**Version:** 1.0
**Fecha:** 2026-01-28
**Estado:** Planificado
---
## 1. Vision General
El Sistema de Goals permite a los usuarios definir, seguir y proyectar metas financieras a largo plazo como retiro, compra de casa, educacion de hijos, etc. Integra simulaciones Monte Carlo para proyectar probabilidades de exito.
### 1.1 Tipos de Metas Soportadas
| Tipo | Icono | Descripcion | Horizonte Tipico |
|------|-------|-------------|------------------|
| `retirement` | 🏖️ | Retiro/Jubilacion | 10-30 anos |
| `home` | 🏠 | Compra de casa/enganche | 2-10 anos |
| `education` | 🎓 | Educacion de hijos | 5-18 anos |
| `emergency` | 🚨 | Fondo de emergencia | 1-2 anos |
| `travel` | ✈️ | Viaje mayor | 1-5 anos |
| `vehicle` | 🚗 | Compra de vehiculo | 1-5 anos |
| `wedding` | 💍 | Boda/evento | 1-3 anos |
| `custom` | 🎯 | Meta personalizada | Variable |
---
## 2. Modelo de Datos
### 2.1 Entity: InvestmentGoal
```typescript
@Entity('investment_goals')
export class InvestmentGoal {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'user_id' })
@Index()
userId: string;
@Column({ length: 100 })
name: string;
@Column({ length: 500, nullable: true })
description: string;
@Column({
type: 'enum',
enum: GoalType,
default: GoalType.CUSTOM,
})
type: GoalType;
@Column('decimal', { precision: 18, scale: 2, name: 'target_amount' })
targetAmount: number;
@Column('decimal', { precision: 18, scale: 2, name: 'current_amount', default: 0 })
currentAmount: number;
@Column('decimal', { precision: 18, scale: 2, name: 'initial_amount', default: 0 })
initialAmount: number;
@Column('decimal', { precision: 18, scale: 2, name: 'monthly_contribution', default: 0 })
monthlyContribution: number;
@Column({ name: 'target_date', type: 'date' })
targetDate: Date;
@Column('decimal', { precision: 5, scale: 2, name: 'expected_return', default: 7.0 })
expectedReturn: number; // Annual return %
@Column('decimal', { precision: 5, scale: 2, name: 'expected_volatility', default: 15.0 })
expectedVolatility: number; // Annual volatility %
@Column({
type: 'enum',
enum: GoalStatus,
default: GoalStatus.ON_TRACK,
})
status: GoalStatus;
@Column({
type: 'enum',
enum: GoalPriority,
default: GoalPriority.MEDIUM,
})
priority: GoalPriority;
@Column({ name: 'linked_account_id', type: 'uuid', nullable: true })
linkedAccountId: string | null;
@Column({ name: 'is_active', default: true })
isActive: boolean;
@Column('jsonb', { name: 'risk_profile', nullable: true })
riskProfile: GoalRiskProfile | null;
@Column('jsonb', { name: 'last_projection', nullable: true })
lastProjection: GoalProjection | null;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt: Date;
@OneToMany(() => GoalContribution, (c) => c.goal)
contributions: GoalContribution[];
@OneToMany(() => GoalMilestone, (m) => m.goal)
milestones: GoalMilestone[];
}
```
### 2.2 Enums
```typescript
enum GoalType {
RETIREMENT = 'retirement',
HOME = 'home',
EDUCATION = 'education',
EMERGENCY = 'emergency',
TRAVEL = 'travel',
VEHICLE = 'vehicle',
WEDDING = 'wedding',
CUSTOM = 'custom',
}
enum GoalStatus {
ON_TRACK = 'on_track',
AHEAD = 'ahead',
BEHIND = 'behind',
AT_RISK = 'at_risk',
ACHIEVED = 'achieved',
PAUSED = 'paused',
}
enum GoalPriority {
HIGH = 'high',
MEDIUM = 'medium',
LOW = 'low',
}
```
### 2.3 Entity: GoalContribution
```typescript
@Entity('goal_contributions')
export class GoalContribution {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'goal_id' })
goalId: string;
@ManyToOne(() => InvestmentGoal, (g) => g.contributions)
@JoinColumn({ name: 'goal_id' })
goal: InvestmentGoal;
@Column('decimal', { precision: 18, scale: 2 })
amount: number;
@Column({
type: 'enum',
enum: ContributionType,
default: ContributionType.DEPOSIT,
})
type: ContributionType;
@Column({ nullable: true })
source: string; // 'manual', 'automatic', 'dividend', 'interest'
@Column({ type: 'text', nullable: true })
notes: string;
@Column({ name: 'transaction_id', nullable: true })
transactionId: string;
@CreateDateColumn({ name: 'contributed_at' })
contributedAt: Date;
}
enum ContributionType {
DEPOSIT = 'deposit',
WITHDRAWAL = 'withdrawal',
INTEREST = 'interest',
DIVIDEND = 'dividend',
ADJUSTMENT = 'adjustment',
}
```
### 2.4 Entity: GoalMilestone
```typescript
@Entity('goal_milestones')
export class GoalMilestone {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ name: 'goal_id' })
goalId: string;
@ManyToOne(() => InvestmentGoal, (g) => g.milestones)
@JoinColumn({ name: 'goal_id' })
goal: InvestmentGoal;
@Column({ length: 100 })
name: string;
@Column('decimal', { precision: 18, scale: 2, name: 'target_amount' })
targetAmount: number;
@Column('decimal', { precision: 5, scale: 2, name: 'target_percentage' })
targetPercentage: number; // % of goal
@Column({ name: 'target_date', type: 'date', nullable: true })
targetDate: Date | null;
@Column({ name: 'achieved_at', type: 'timestamp', nullable: true })
achievedAt: Date | null;
@Column({ default: false })
isAchieved: boolean;
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
}
```
---
## 3. Interfaces de Proyeccion
### 3.1 GoalProjection
```typescript
interface GoalProjection {
// Metadata
goalId: string;
calculatedAt: Date;
// Current State
currentAmount: number;
targetAmount: number;
progressPercent: number;
monthsRemaining: number;
// Projections
projectedValue: number;
projectedProgress: number;
shortfall: number;
// Probability Analysis
probabilityOfSuccess: number;
confidenceInterval: {
low: number; // P10
mid: number; // P50 (median)
high: number; // P90
};
// Scenarios
scenarios: GoalScenarios;
// Recommendations
requiredMonthlyContribution: number;
suggestedActions: string[];
}
interface GoalScenarios {
optimistic: ScenarioResult;
base: ScenarioResult;
pessimistic: ScenarioResult;
}
interface ScenarioResult {
projectedValue: number;
endDate: Date;
returnRate: number;
probabilityOfSuccess: number;
}
```
### 3.2 MonteCarloParams
```typescript
interface MonteCarloParams {
initialAmount: number;
monthlyContribution: number;
expectedReturn: number; // Annual, e.g., 0.07 for 7%
volatility: number; // Annual, e.g., 0.15 for 15%
months: number;
numSimulations: number; // Default: 10000
targetAmount: number;
}
interface MonteCarloResult {
simulations: number[][]; // [sim][month]
finalValues: number[]; // Final value per simulation
percentiles: {
p10: number;
p25: number;
p50: number;
p75: number;
p90: number;
p95: number;
};
probabilityOfSuccess: number;
averageFinalValue: number;
medianFinalValue: number;
}
```
---
## 4. Servicio de Goals
### 4.1 GoalService
```typescript
@Injectable()
export class GoalService {
constructor(
@InjectRepository(InvestmentGoal)
private goalRepo: Repository<InvestmentGoal>,
@InjectRepository(GoalContribution)
private contributionRepo: Repository<GoalContribution>,
private projectionService: GoalProjectionService,
private eventEmitter: EventEmitter2,
) {}
async createGoal(dto: CreateGoalDto): Promise<InvestmentGoal> {
const goal = this.goalRepo.create({
...dto,
status: GoalStatus.ON_TRACK,
currentAmount: dto.initialAmount || 0,
});
// Create default milestones
goal.milestones = this.createDefaultMilestones(goal);
const saved = await this.goalRepo.save(goal);
// Calculate initial projection
const projection = await this.projectionService.calculateProjection(saved);
saved.lastProjection = projection;
saved.status = this.determineStatus(projection);
await this.goalRepo.save(saved);
this.eventEmitter.emit('goal.created', saved);
return saved;
}
async addContribution(
goalId: string,
dto: AddContributionDto
): Promise<GoalContribution> {
const goal = await this.getGoalById(goalId);
const contribution = this.contributionRepo.create({
goalId,
...dto,
});
await this.contributionRepo.save(contribution);
// Update goal current amount
const delta = dto.type === ContributionType.WITHDRAWAL
? -dto.amount
: dto.amount;
goal.currentAmount = Number(goal.currentAmount) + delta;
// Recalculate projection
const projection = await this.projectionService.calculateProjection(goal);
goal.lastProjection = projection;
goal.status = this.determineStatus(projection);
// Check milestones
await this.checkMilestones(goal);
await this.goalRepo.save(goal);
this.eventEmitter.emit('goal.contribution.added', { goal, contribution });
return contribution;
}
async getGoalsByUser(userId: string): Promise<InvestmentGoal[]> {
return this.goalRepo.find({
where: { userId, isActive: true },
relations: ['contributions', 'milestones'],
order: { priority: 'ASC', targetDate: 'ASC' },
});
}
async getGoalProgress(goalId: string): Promise<GoalProgressDto> {
const goal = await this.getGoalById(goalId);
const projection = await this.projectionService.calculateProjection(goal);
const contributions = await this.contributionRepo.find({
where: { goalId },
order: { contributedAt: 'DESC' },
take: 12, // Last 12 contributions
});
return {
goal,
projection,
contributions,
milestones: goal.milestones.map(m => ({
...m,
progress: (goal.currentAmount / m.targetAmount) * 100,
})),
};
}
private createDefaultMilestones(goal: InvestmentGoal): GoalMilestone[] {
return [
{ name: '25% alcanzado', targetPercentage: 25, targetAmount: goal.targetAmount * 0.25 },
{ name: '50% alcanzado', targetPercentage: 50, targetAmount: goal.targetAmount * 0.50 },
{ name: '75% alcanzado', targetPercentage: 75, targetAmount: goal.targetAmount * 0.75 },
{ name: 'Meta completada', targetPercentage: 100, targetAmount: goal.targetAmount },
].map(m => this.milestoneRepo.create({ ...m, goalId: goal.id }));
}
private determineStatus(projection: GoalProjection): GoalStatus {
if (projection.probabilityOfSuccess >= 95) return GoalStatus.AHEAD;
if (projection.probabilityOfSuccess >= 75) return GoalStatus.ON_TRACK;
if (projection.probabilityOfSuccess >= 50) return GoalStatus.BEHIND;
return GoalStatus.AT_RISK;
}
}
```
---
## 5. Servicio de Proyeccion
### 5.1 GoalProjectionService
```typescript
@Injectable()
export class GoalProjectionService {
async calculateProjection(goal: InvestmentGoal): Promise<GoalProjection> {
const monthsRemaining = this.getMonthsRemaining(goal.targetDate);
// Deterministic projection (Future Value formula)
const projectedValue = this.calculateFutureValue(
goal.currentAmount,
goal.monthlyContribution,
goal.expectedReturn / 100 / 12, // Monthly rate
monthsRemaining
);
// Monte Carlo simulation
const mcResult = await this.runMonteCarloSimulation({
initialAmount: Number(goal.currentAmount),
monthlyContribution: Number(goal.monthlyContribution),
expectedReturn: goal.expectedReturn / 100,
volatility: goal.expectedVolatility / 100,
months: monthsRemaining,
numSimulations: 10000,
targetAmount: Number(goal.targetAmount),
});
const progressPercent = (goal.currentAmount / goal.targetAmount) * 100;
const projectedProgress = (projectedValue / goal.targetAmount) * 100;
return {
goalId: goal.id,
calculatedAt: new Date(),
currentAmount: Number(goal.currentAmount),
targetAmount: Number(goal.targetAmount),
progressPercent,
monthsRemaining,
projectedValue,
projectedProgress,
shortfall: Math.max(0, Number(goal.targetAmount) - projectedValue),
probabilityOfSuccess: mcResult.probabilityOfSuccess,
confidenceInterval: {
low: mcResult.percentiles.p10,
mid: mcResult.percentiles.p50,
high: mcResult.percentiles.p90,
},
scenarios: this.calculateScenarios(goal, monthsRemaining),
requiredMonthlyContribution: this.calculateRequiredContribution(goal),
suggestedActions: this.generateSuggestions(goal, mcResult.probabilityOfSuccess),
};
}
/**
* Future Value with regular deposits
* FV = PV(1+r)^n + PMT × ((1+r)^n - 1) / r
*/
private calculateFutureValue(
presentValue: number,
monthlyPayment: number,
monthlyRate: number,
months: number
): number {
if (monthlyRate === 0) {
return presentValue + monthlyPayment * months;
}
const growthFactor = Math.pow(1 + monthlyRate, months);
const fvOfPresentValue = presentValue * growthFactor;
const fvOfPayments = monthlyPayment * ((growthFactor - 1) / monthlyRate);
return fvOfPresentValue + fvOfPayments;
}
/**
* Monte Carlo Simulation
* Geometric Brownian Motion: S(t+1) = S(t) * exp((mu - sigma^2/2)*dt + sigma*sqrt(dt)*Z)
*/
async runMonteCarloSimulation(params: MonteCarloParams): Promise<MonteCarloResult> {
const { initialAmount, monthlyContribution, expectedReturn, volatility, months, numSimulations, targetAmount } = params;
const monthlyReturn = expectedReturn / 12;
const monthlyVolatility = volatility / Math.sqrt(12);
const dt = 1; // 1 month
const simulations: number[][] = [];
const finalValues: number[] = [];
for (let sim = 0; sim < numSimulations; sim++) {
const path: number[] = [initialAmount];
let value = initialAmount;
for (let month = 1; month <= months; month++) {
// Random normal (Box-Muller transform)
const z = this.randomNormal();
// GBM step
const drift = (monthlyReturn - 0.5 * monthlyVolatility ** 2) * dt;
const diffusion = monthlyVolatility * Math.sqrt(dt) * z;
value = value * Math.exp(drift + diffusion) + monthlyContribution;
path.push(Math.max(0, value)); // No negative values
}
simulations.push(path);
finalValues.push(path[path.length - 1]);
}
// Sort for percentile calculation
const sorted = [...finalValues].sort((a, b) => a - b);
const percentile = (p: number) => sorted[Math.floor(p * numSimulations)];
const successCount = finalValues.filter(v => v >= targetAmount).length;
return {
simulations,
finalValues,
percentiles: {
p10: percentile(0.10),
p25: percentile(0.25),
p50: percentile(0.50),
p75: percentile(0.75),
p90: percentile(0.90),
p95: percentile(0.95),
},
probabilityOfSuccess: (successCount / numSimulations) * 100,
averageFinalValue: finalValues.reduce((a, b) => a + b, 0) / numSimulations,
medianFinalValue: percentile(0.50),
};
}
/**
* Box-Muller transform for normal distribution
*/
private randomNormal(): number {
const u1 = Math.random();
const u2 = Math.random();
return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
}
/**
* Calculate required monthly contribution to reach target
* PMT = (FV - PV(1+r)^n) × r / ((1+r)^n - 1)
*/
calculateRequiredContribution(goal: InvestmentGoal): number {
const months = this.getMonthsRemaining(goal.targetDate);
const monthlyRate = goal.expectedReturn / 100 / 12;
if (months <= 0) return 0;
if (monthlyRate === 0) {
return (Number(goal.targetAmount) - Number(goal.currentAmount)) / months;
}
const growthFactor = Math.pow(1 + monthlyRate, months);
const fvOfCurrent = Number(goal.currentAmount) * growthFactor;
const remaining = Number(goal.targetAmount) - fvOfCurrent;
if (remaining <= 0) return 0;
const factor = (growthFactor - 1) / monthlyRate;
return remaining / factor;
}
private calculateScenarios(goal: InvestmentGoal, months: number): GoalScenarios {
return {
optimistic: this.projectWithReturn(goal, months, goal.expectedReturn + 3),
base: this.projectWithReturn(goal, months, goal.expectedReturn),
pessimistic: this.projectWithReturn(goal, months, goal.expectedReturn - 3),
};
}
private projectWithReturn(goal: InvestmentGoal, months: number, annualReturn: number): ScenarioResult {
const monthlyRate = annualReturn / 100 / 12;
const projectedValue = this.calculateFutureValue(
Number(goal.currentAmount),
Number(goal.monthlyContribution),
monthlyRate,
months
);
return {
projectedValue,
endDate: goal.targetDate,
returnRate: annualReturn,
probabilityOfSuccess: projectedValue >= Number(goal.targetAmount) ? 100 : (projectedValue / Number(goal.targetAmount)) * 100,
};
}
private generateSuggestions(goal: InvestmentGoal, probability: number): string[] {
const suggestions: string[] = [];
if (probability < 50) {
const required = this.calculateRequiredContribution(goal);
const increase = required - Number(goal.monthlyContribution);
if (increase > 0) {
suggestions.push(`Incrementar aportacion mensual en $${increase.toFixed(0)} para alcanzar meta`);
}
suggestions.push('Considerar extender la fecha objetivo');
suggestions.push('Evaluar perfil de riesgo para mayor rendimiento esperado');
} else if (probability < 75) {
suggestions.push('Meta en riesgo. Considerar aumentar aportaciones');
} else if (probability < 95) {
suggestions.push('Meta en buen camino. Mantener aportaciones actuales');
} else {
suggestions.push('Excelente progreso! Meta muy probable de alcanzar');
}
return suggestions;
}
private getMonthsRemaining(targetDate: Date): number {
const now = new Date();
const target = new Date(targetDate);
return Math.max(0, (target.getFullYear() - now.getFullYear()) * 12 +
(target.getMonth() - now.getMonth()));
}
}
```
---
## 6. API Endpoints
### 6.1 Goals CRUD
| Method | Endpoint | Descripcion |
|--------|----------|-------------|
| GET | `/api/goals` | Lista de metas del usuario |
| POST | `/api/goals` | Crear nueva meta |
| GET | `/api/goals/:id` | Detalle de meta |
| PUT | `/api/goals/:id` | Actualizar meta |
| DELETE | `/api/goals/:id` | Eliminar meta |
| PATCH | `/api/goals/:id/pause` | Pausar meta |
| PATCH | `/api/goals/:id/resume` | Reanudar meta |
### 6.2 Contributions
| Method | Endpoint | Descripcion |
|--------|----------|-------------|
| GET | `/api/goals/:id/contributions` | Historial de contribuciones |
| POST | `/api/goals/:id/contributions` | Agregar contribucion |
### 6.3 Projections & Analytics
| Method | Endpoint | Descripcion |
|--------|----------|-------------|
| GET | `/api/goals/:id/progress` | Progreso con proyeccion |
| GET | `/api/goals/:id/projection` | Proyeccion Monte Carlo |
| POST | `/api/goals/:id/simulate` | Simular escenarios custom |
| GET | `/api/goals/:id/milestones` | Estado de milestones |
---
## 7. Componentes Frontend
### 7.1 GoalDashboard
```typescript
interface GoalDashboardProps {
userId: string;
}
// Componentes hijos:
// - GoalSummaryCard: Resumen de todas las metas
// - GoalList: Lista de metas con progreso
// - AddGoalButton: Crear nueva meta
```
### 7.2 GoalCard
```typescript
interface GoalCardProps {
goal: InvestmentGoal;
onEdit: () => void;
onContribute: () => void;
}
// Muestra:
// - Nombre e icono del tipo
// - Barra de progreso visual
// - Monto actual / objetivo
// - Fecha objetivo
// - Estado (on_track, behind, etc.)
// - Probabilidad de exito
```
### 7.3 GoalProjectionChart
```typescript
interface GoalProjectionChartProps {
projection: GoalProjection;
height?: number;
}
// Grafico que muestra:
// - Linea de progreso actual
// - Bandas de confianza (P10, P50, P90)
// - Linea objetivo
// - Fecha objetivo
```
### 7.4 GoalWizard
```typescript
interface GoalWizardProps {
onComplete: (goal: InvestmentGoal) => void;
}
// Pasos:
// 1. Seleccionar tipo de meta
// 2. Definir nombre y monto objetivo
// 3. Fecha objetivo
// 4. Aportacion mensual inicial
// 5. Perfil de riesgo (conservador/moderado/agresivo)
// 6. Confirmar y ver proyeccion inicial
```
---
## 8. Configuraciones por Tipo de Meta
### 8.1 Defaults por Tipo
```typescript
const GOAL_DEFAULTS: Record<GoalType, GoalDefaults> = {
retirement: {
expectedReturn: 7.0,
expectedVolatility: 12.0,
suggestedHorizon: '20+ years',
riskProfile: 'moderate',
},
home: {
expectedReturn: 5.0,
expectedVolatility: 8.0,
suggestedHorizon: '3-7 years',
riskProfile: 'conservative',
},
education: {
expectedReturn: 6.0,
expectedVolatility: 10.0,
suggestedHorizon: '5-18 years',
riskProfile: 'moderate',
},
emergency: {
expectedReturn: 2.0,
expectedVolatility: 2.0,
suggestedHorizon: '6-12 months',
riskProfile: 'conservative',
},
travel: {
expectedReturn: 4.0,
expectedVolatility: 5.0,
suggestedHorizon: '1-3 years',
riskProfile: 'conservative',
},
vehicle: {
expectedReturn: 4.0,
expectedVolatility: 6.0,
suggestedHorizon: '1-5 years',
riskProfile: 'conservative',
},
wedding: {
expectedReturn: 3.0,
expectedVolatility: 4.0,
suggestedHorizon: '1-3 years',
riskProfile: 'conservative',
},
custom: {
expectedReturn: 5.0,
expectedVolatility: 10.0,
suggestedHorizon: 'Variable',
riskProfile: 'moderate',
},
};
```
---
## 9. Notificaciones y Alertas
### 9.1 Eventos de Notificacion
| Evento | Trigger | Canal |
|--------|---------|-------|
| `goal.milestone.reached` | Progreso >= milestone % | Push, Email |
| `goal.status.changed` | Status cambia a behind/at_risk | Push, Email |
| `goal.achieved` | Monto actual >= objetivo | Push, Email, Celebracion UI |
| `goal.contribution.reminder` | Dia de aportacion programada | Push |
| `goal.review.monthly` | Primer dia del mes | Email digest |
### 9.2 Configuracion de Alertas
```typescript
interface GoalAlertSettings {
goalId: string;
milestoneAlerts: boolean;
statusChangeAlerts: boolean;
contributionReminders: boolean;
reminderDayOfMonth: number; // 1-28
channels: ('push' | 'email' | 'sms')[];
}
```
---
## 10. Referencias
- [ET-PFM-007: Motor de Metas](./ET-PFM-007-motor-metas.md)
- [RF-PFM-007: Metas de Inversion](../requerimientos/RF-PFM-007-metas-inversion.md)
- [US-PFM-012: Reporte Fiscal](../historias-usuario/US-PFM-012-reporte-fiscal.md)
- [TRACEABILITY.yml](../implementacion/TRACEABILITY.yml)
---
*Especificacion tecnica - Sistema NEXUS*