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

23 KiB
Raw Blame History

id title type status priority epic project version created_date updated_date
ET-PFM-011 Sistema de Goals (Metas de Inversion) Technical Specification Done Alta OQI-008 trading-platform 1.0.0 2026-01-28 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

@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

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

@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

@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

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

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

@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

@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

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

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

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

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

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

interface GoalAlertSettings {
  goalId: string;
  milestoneAlerts: boolean;
  statusChangeAlerts: boolean;
  contributionReminders: boolean;
  reminderDayOfMonth: number; // 1-28
  channels: ('push' | 'email' | 'sms')[];
}

10. Referencias


Especificacion tecnica - Sistema NEXUS