# ET-PFM-007: Motor de Metas de Inversión

**Épica:** OQI-008 - Portfolio Manager
**Versión:** 1.0
**Fecha:** 2025-12-05
**Estado:** Planificado

---

## Modelo de Datos

```typescript
@Entity('investment_goals')
export class InvestmentGoal {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ name: 'user_id' })
  userId: string;

  @Column({ length: 100 })
  name: string;

  @Column({
    type: 'enum',
    enum: ['retirement', 'home', 'education', 'emergency', 'custom'],
  })
  type: GoalType;

  @Column('decimal', { precision: 18, scale: 2, name: 'target_amount' })
  targetAmount: number;

  @Column('decimal', { precision: 18, scale: 2, name: 'current_amount' })
  currentAmount: number;

  @Column('decimal', { precision: 18, scale: 2, name: 'monthly_contribution' })
  monthlyContribution: number;

  @Column({ name: 'target_date' })
  targetDate: Date;

  @Column('decimal', { precision: 5, scale: 2, name: 'expected_return' })
  expectedReturn: number;

  @Column({
    type: 'enum',
    enum: ['on_track', 'ahead', 'behind'],
    default: 'on_track',
  })
  status: GoalStatus;

  @Column({ name: 'linked_account_id', nullable: true })
  linkedAccountId: string;

  @CreateDateColumn({ name: 'created_at' })
  createdAt: Date;

  @UpdateDateColumn({ name: 'updated_at' })
  updatedAt: Date;
}
```

---

## Servicio de Proyección

```typescript
@Injectable()
export class GoalProjectionService {

  calculateProjection(goal: InvestmentGoal): GoalProjection {
    const monthsRemaining = this.getMonthsRemaining(goal.targetDate);
    const monthlyReturn = goal.expectedReturn / 12 / 100;

    // Future Value con contribuciones mensuales
    // FV = PV(1+r)^n + PMT × ((1+r)^n - 1) / r
    const futureValue =
      goal.currentAmount * Math.pow(1 + monthlyReturn, monthsRemaining) +
      goal.monthlyContribution *
        ((Math.pow(1 + monthlyReturn, monthsRemaining) - 1) / monthlyReturn);

    const progressPercent = (goal.currentAmount / goal.targetAmount) * 100;
    const projectedPercent = (futureValue / goal.targetAmount) * 100;

    return {
      currentProgress: progressPercent,
      projectedValue: futureValue,
      projectedProgress: projectedPercent,
      shortfall: Math.max(0, goal.targetAmount - futureValue),
      status: this.determineStatus(projectedPercent),
      scenarios: this.calculateScenarios(goal, monthsRemaining),
    };
  }

  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),
    };
  }

  calculateRequiredContribution(goal: InvestmentGoal): number {
    const months = this.getMonthsRemaining(goal.targetDate);
    const monthlyReturn = goal.expectedReturn / 12 / 100;

    // PMT = (FV - PV(1+r)^n) × r / ((1+r)^n - 1)
    const fvOfCurrent = goal.currentAmount * Math.pow(1 + monthlyReturn, months);
    const remaining = goal.targetAmount - fvOfCurrent;
    const factor = (Math.pow(1 + monthlyReturn, months) - 1) / monthlyReturn;

    return remaining / factor;
  }
}
```

---

## API Endpoints

| Method | Endpoint | Descripción |
|--------|----------|-------------|
| GET | `/api/goals` | Lista de metas |
| POST | `/api/goals` | Crear meta |
| GET | `/api/goals/:id` | Detalle de meta |
| PUT | `/api/goals/:id` | Actualizar meta |
| DELETE | `/api/goals/:id` | Eliminar meta |
| GET | `/api/goals/:id/projection` | Proyección de meta |
| POST | `/api/goals/:id/simulate` | Simular escenarios |

---

## Referencias

- [RF-PFM-007: Metas de Inversión](../requerimientos/RF-PFM-007-metas-inversión.md)

---

*Especificación técnica - Sistema NEXUS*
