webmoney/backend/app/Http/Controllers/Api/FinancialHealthController.php
marcoitaloesp-ai 604302ada4
fix: Change transaction_type to type in controllers and models
- FinancialHealthController: Fix column name in queries
- ReportController: Fix column name in queries
- Budget model: Fix getSpentAmountAttribute query
2025-12-14 16:36:31 +00:00

499 lines
19 KiB
PHP

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Transaction;
use App\Models\Account;
use App\Models\LiabilityAccount;
use App\Models\Budget;
use App\Models\FinancialGoal;
use App\Models\RecurringTemplate;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
class FinancialHealthController extends Controller
{
/**
* Obtener puntuación de salud financiera completa
*/
public function index(Request $request)
{
$userId = Auth::id();
// Calcular cada métrica
$savingsCapacity = $this->calculateSavingsCapacity($userId);
$debtControl = $this->calculateDebtControl($userId);
$budgetManagement = $this->calculateBudgetManagement($userId);
$investments = $this->calculateInvestments($userId);
$emergencyFund = $this->calculateEmergencyFund($userId);
$futurePlanning = $this->calculateFuturePlanning($userId);
// Puntuación general ponderada
$weights = [
'savings' => 0.25,
'debt' => 0.20,
'budget' => 0.15,
'investments' => 0.15,
'emergency' => 0.15,
'planning' => 0.10,
];
$overallScore = round(
($savingsCapacity['score'] * $weights['savings']) +
($debtControl['score'] * $weights['debt']) +
($budgetManagement['score'] * $weights['budget']) +
($investments['score'] * $weights['investments']) +
($emergencyFund['score'] * $weights['emergency']) +
($futurePlanning['score'] * $weights['planning'])
);
// Determinar nivel de salud
$healthLevel = $this->getHealthLevel($overallScore);
// Generar insights y recomendaciones
$insights = $this->generateInsights($userId, [
'savings' => $savingsCapacity,
'debt' => $debtControl,
'budget' => $budgetManagement,
'investments' => $investments,
'emergency' => $emergencyFund,
'planning' => $futurePlanning,
]);
return response()->json([
'overall_score' => $overallScore,
'health_level' => $healthLevel,
'last_updated' => now()->format('Y-m-d'),
'metrics' => [
'savings_capacity' => $savingsCapacity,
'debt_control' => $debtControl,
'budget_management' => $budgetManagement,
'investments' => $investments,
'emergency_fund' => $emergencyFund,
'future_planning' => $futurePlanning,
],
'insights' => $insights,
]);
}
/**
* Calcular capacidad de ahorro
*/
private function calculateSavingsCapacity($userId)
{
// Últimos 3 meses
$data = Transaction::where('user_id', $userId)
->where('transaction_date', '>=', now()->subMonths(3)->startOfMonth())
->selectRaw("
SUM(CASE WHEN type = 'credit' THEN amount ELSE 0 END) as income,
SUM(CASE WHEN type = 'debit' THEN ABS(amount) ELSE 0 END) as expense
")
->first();
$income = $data->income ?? 0;
$expense = $data->expense ?? 0;
$savings = $income - $expense;
$savingsRate = $income > 0 ? ($savings / $income) * 100 : 0;
// Puntuación basada en tasa de ahorro
// 0-10% = 40, 10-20% = 60, 20-30% = 80, 30%+ = 100
$score = match(true) {
$savingsRate >= 30 => 100,
$savingsRate >= 20 => 80 + (($savingsRate - 20) * 2),
$savingsRate >= 10 => 60 + (($savingsRate - 10) * 2),
$savingsRate >= 0 => 40 + ($savingsRate * 2),
default => max(0, 40 + $savingsRate),
};
return [
'score' => round(min(100, max(0, $score))),
'savings_rate' => round($savingsRate, 1),
'monthly_savings' => round($savings / 3, 2),
'status' => $savingsRate >= 20 ? 'excellent' : ($savingsRate >= 10 ? 'good' : 'needs_improvement'),
'message' => $savingsRate >= 20
? "¡Excelente! Ahorras el {$savingsRate}% de tus ingresos"
: ($savingsRate >= 10
? "Buen trabajo. Ahorras el {$savingsRate}%"
: "Intenta aumentar tu tasa de ahorro"),
];
}
/**
* Calcular control de deudas
*/
private function calculateDebtControl($userId)
{
// Obtener deudas activas
$liabilities = LiabilityAccount::where('user_id', $userId)
->where('status', 'active')
->get();
$totalDebt = $liabilities->sum('current_balance');
$totalCreditLimit = $liabilities->sum('credit_limit');
$monthlyPayments = $liabilities->sum('monthly_payment');
// Ingresos mensuales promedio
$monthlyIncome = Transaction::where('user_id', $userId)
->where('type', 'credit')
->where('transaction_date', '>=', now()->subMonths(3)->startOfMonth())
->sum('amount') / 3;
// Ratio deuda/ingresos
$debtToIncomeRatio = $monthlyIncome > 0 ? ($monthlyPayments / $monthlyIncome) * 100 : 0;
// Utilización de crédito
$creditUtilization = $totalCreditLimit > 0 ? ($totalDebt / $totalCreditLimit) * 100 : 0;
// Puntuación
// DTI < 20% = excelente, 20-35% = bueno, 35-50% = moderado, >50% = alto riesgo
$score = match(true) {
$debtToIncomeRatio <= 20 => 100 - ($debtToIncomeRatio * 0.5),
$debtToIncomeRatio <= 35 => 80 - (($debtToIncomeRatio - 20) * 1.3),
$debtToIncomeRatio <= 50 => 60 - (($debtToIncomeRatio - 35) * 2),
default => max(0, 30 - (($debtToIncomeRatio - 50) * 0.6)),
};
return [
'score' => round(min(100, max(0, $score))),
'total_debt' => round($totalDebt, 2),
'monthly_payments' => round($monthlyPayments, 2),
'debt_to_income_ratio' => round($debtToIncomeRatio, 1),
'credit_utilization' => round($creditUtilization, 1),
'active_debts' => $liabilities->count(),
'status' => $debtToIncomeRatio <= 20 ? 'excellent' : ($debtToIncomeRatio <= 35 ? 'acceptable' : 'needs_attention'),
'message' => $totalDebt == 0
? "¡Sin deudas! Excelente situación"
: ($debtToIncomeRatio <= 35
? "Nivel aceptable. Crédito disponible: " . number_format($totalCreditLimit - $totalDebt, 2) . ""
: "Considera reducir tus deudas"),
];
}
/**
* Calcular gestión de presupuesto
*/
private function calculateBudgetManagement($userId)
{
$currentMonth = now()->month;
$currentYear = now()->year;
$budgets = Budget::where('user_id', $userId)
->where('year', $currentYear)
->where('month', $currentMonth)
->where('is_active', true)
->get();
if ($budgets->isEmpty()) {
return [
'score' => 50,
'has_budgets' => false,
'status' => 'not_configured',
'message' => 'No tienes presupuestos configurados',
'exceeded_count' => 0,
'total_budgets' => 0,
];
}
$exceededCount = $budgets->filter(fn($b) => $b->is_exceeded)->count();
$totalBudgets = $budgets->count();
$complianceRate = (($totalBudgets - $exceededCount) / $totalBudgets) * 100;
// También verificar el uso promedio
$avgUsage = $budgets->avg('usage_percentage');
// Puntuación
$score = match(true) {
$exceededCount == 0 && $avgUsage <= 90 => 100,
$exceededCount == 0 => 85,
$exceededCount <= 1 => 70,
$exceededCount <= 2 => 55,
default => max(20, 55 - ($exceededCount * 10)),
};
// Encontrar categoría más excedida
$mostExceeded = $budgets->filter(fn($b) => $b->is_exceeded)
->sortByDesc('usage_percentage')
->first();
return [
'score' => round($score),
'has_budgets' => true,
'total_budgets' => $totalBudgets,
'exceeded_count' => $exceededCount,
'compliance_rate' => round($complianceRate, 1),
'avg_usage' => round($avgUsage, 1),
'most_exceeded' => $mostExceeded ? [
'category' => $mostExceeded->category?->name ?? $mostExceeded->name,
'usage' => $mostExceeded->usage_percentage,
] : null,
'status' => $exceededCount == 0 ? 'on_track' : ($exceededCount <= 2 ? 'needs_attention' : 'exceeded'),
'message' => $exceededCount == 0
? "Todos los presupuestos bajo control"
: "¡Atención! Excediste presupuesto en " . ($mostExceeded->category?->name ?? 'una categoría'),
];
}
/**
* Calcular inversiones
*/
private function calculateInvestments($userId)
{
// Cuentas de tipo inversión
$investmentAccounts = Account::where('user_id', $userId)
->where('type', 'investment')
->get();
$totalInvestments = $investmentAccounts->sum('current_balance');
// Total de activos
$totalAssets = Account::where('user_id', $userId)
->where('include_in_total', true)
->sum('current_balance');
$investmentRatio = $totalAssets > 0 ? ($totalInvestments / $totalAssets) * 100 : 0;
// Puntuación basada en ratio de inversión
$score = match(true) {
$investmentRatio >= 30 => 100,
$investmentRatio >= 20 => 80 + (($investmentRatio - 20) * 2),
$investmentRatio >= 10 => 60 + (($investmentRatio - 10) * 2),
$investmentRatio >= 5 => 40 + (($investmentRatio - 5) * 4),
default => max(20, $investmentRatio * 8),
};
return [
'score' => round(min(100, $score)),
'total_investments' => round($totalInvestments, 2),
'investment_ratio' => round($investmentRatio, 1),
'accounts_count' => $investmentAccounts->count(),
'status' => $investmentRatio >= 20 ? 'good' : ($investmentRatio >= 10 ? 'moderate' : 'low'),
'message' => $investmentRatio >= 20
? "Buena diversificación. Sigue así"
: "Considera aumentar tu inversión",
];
}
/**
* Calcular fondo de emergencia
*/
private function calculateEmergencyFund($userId)
{
// Cuentas líquidas (checking, savings, cash)
$liquidAccounts = Account::where('user_id', $userId)
->whereIn('type', ['checking', 'savings', 'cash'])
->where('include_in_total', true)
->sum('current_balance');
// Gastos mensuales promedio
$monthlyExpenses = Transaction::where('user_id', $userId)
->where('type', 'debit')
->where('transaction_date', '>=', now()->subMonths(3)->startOfMonth())
->sum(fn($t) => abs($t->amount)) / 3;
// Usar consulta directa para mejor rendimiento
$monthlyExpenses = abs(Transaction::where('user_id', $userId)
->where('type', 'debit')
->where('transaction_date', '>=', now()->subMonths(3)->startOfMonth())
->sum('amount')) / 3;
// Meses cubiertos
$monthsCovered = $monthlyExpenses > 0 ? $liquidAccounts / $monthlyExpenses : 0;
// Puntuación (ideal: 6 meses)
$score = match(true) {
$monthsCovered >= 6 => 100,
$monthsCovered >= 3 => 60 + (($monthsCovered - 3) * 13.33),
$monthsCovered >= 1 => 30 + (($monthsCovered - 1) * 15),
default => max(0, $monthsCovered * 30),
};
return [
'score' => round(min(100, $score)),
'liquid_assets' => round($liquidAccounts, 2),
'monthly_expenses' => round($monthlyExpenses, 2),
'months_covered' => round($monthsCovered, 1),
'recommended_fund' => round($monthlyExpenses * 6, 2),
'status' => $monthsCovered >= 6 ? 'excellent' : ($monthsCovered >= 3 ? 'moderate' : 'insufficient'),
'message' => $monthsCovered >= 6
? "Excelente fondo de emergencia"
: "Tienes " . number_format($liquidAccounts, 2) . "€ (" . round($monthsCovered, 1) . " meses de gastos)",
];
}
/**
* Calcular planificación futura
*/
private function calculateFuturePlanning($userId)
{
// Metas financieras activas
$goals = FinancialGoal::where('user_id', $userId)
->where('status', 'active')
->get();
// Transacciones recurrentes configuradas
$recurringCount = RecurringTemplate::where('user_id', $userId)
->where('is_active', true)
->count();
// Presupuestos configurados
$hasBudgets = Budget::where('user_id', $userId)
->where('is_active', true)
->exists();
$activeGoals = $goals->count();
$goalsOnTrack = $goals->filter(fn($g) => $g->is_on_track)->count();
$avgProgress = $goals->avg('progress_percentage') ?? 0;
// Puntuación
$baseScore = 40;
if ($activeGoals > 0) $baseScore += 20;
if ($activeGoals >= 3) $baseScore += 10;
if ($goalsOnTrack > 0) $baseScore += 15;
if ($hasBudgets) $baseScore += 10;
if ($recurringCount >= 3) $baseScore += 5;
// Bonus por progreso
$baseScore += min(10, $avgProgress / 10);
return [
'score' => round(min(100, $baseScore)),
'active_goals' => $activeGoals,
'goals_on_track' => $goalsOnTrack,
'avg_progress' => round($avgProgress, 1),
'has_budgets' => $hasBudgets,
'recurring_configured' => $recurringCount,
'status' => $activeGoals >= 3 ? 'excellent' : ($activeGoals >= 1 ? 'good' : 'needs_setup'),
'message' => $activeGoals > 0
? "¡Muy bien! Tienes $activeGoals metas activas en progreso"
: "Crea metas financieras para mejorar tu planificación",
];
}
/**
* Determinar nivel de salud
*/
private function getHealthLevel($score)
{
return match(true) {
$score >= 85 => ['level' => 'excellent', 'label' => 'Excelente Salud', 'color' => '#22c55e'],
$score >= 70 => ['level' => 'good', 'label' => 'Buena Salud', 'color' => '#84cc16'],
$score >= 55 => ['level' => 'moderate', 'label' => 'Salud Moderada', 'color' => '#f59e0b'],
$score >= 40 => ['level' => 'needs_work', 'label' => 'Necesita Mejorar', 'color' => '#f97316'],
default => ['level' => 'critical', 'label' => 'Atención Necesaria', 'color' => '#ef4444'],
};
}
/**
* Generar insights y recomendaciones
*/
private function generateInsights($userId, $metrics)
{
$insights = [];
// Prioridad Alta - Problemas críticos
if ($metrics['debt']['score'] < 50) {
$insights[] = [
'type' => 'high_priority',
'icon' => 'bi-exclamation-triangle-fill',
'color' => '#ef4444',
'title' => 'Prioridad Alta',
'message' => "Reduce gastos en " . ($metrics['budget']['most_exceeded']['category'] ?? 'categorías con exceso') . ". Estás excediendo el presupuesto en " . ($metrics['budget']['most_exceeded']['usage'] ?? 0) . "%.",
];
}
// Prioridad Media - Mejoras recomendadas
if ($metrics['emergency']['months_covered'] < 6) {
$needed = $metrics['emergency']['recommended_fund'] - $metrics['emergency']['liquid_assets'];
$insights[] = [
'type' => 'medium_priority',
'icon' => 'bi-shield-exclamation',
'color' => '#f59e0b',
'title' => 'Prioridad Media',
'message' => "Incrementa tu fondo de emergencia a 6 meses de gastos (" . number_format($metrics['emergency']['recommended_fund'], 0) . "€).",
];
}
// Logros
if ($metrics['savings']['savings_rate'] >= 20) {
$insights[] = [
'type' => 'achievement',
'icon' => 'bi-trophy-fill',
'color' => '#22c55e',
'title' => 'Logro Destacado',
'message' => "¡Excelente ahorro mensual! Mantienes una tasa del " . $metrics['savings']['savings_rate'] . "% por encima de la meta.",
];
}
// Oportunidades
if ($metrics['investments']['investment_ratio'] < 20 && $metrics['savings']['savings_rate'] > 15) {
$insights[] = [
'type' => 'opportunity',
'icon' => 'bi-lightbulb-fill',
'color' => '#3b82f6',
'title' => 'Oportunidad',
'message' => "Considera aumentar tu inversión con el excedente de tus ahorros.",
];
}
// Metas próximas
$upcomingGoals = FinancialGoal::where('user_id', $userId)
->where('status', 'active')
->whereNotNull('target_date')
->where('target_date', '<=', now()->addMonths(6))
->orderBy('target_date')
->first();
if ($upcomingGoals) {
$insights[] = [
'type' => 'upcoming_goal',
'icon' => 'bi-calendar-check-fill',
'color' => '#8b5cf6',
'title' => 'Meta Próxima',
'message' => "Faltan " . now()->diffInMonths($upcomingGoals->target_date) . " meses para tu meta de " . $upcomingGoals->name . ". ¡Sigue así!",
];
}
// Sugerencia general
if ($metrics['planning']['active_goals'] < 3) {
$insights[] = [
'type' => 'suggestion',
'icon' => 'bi-gear-fill',
'color' => '#06b6d4',
'title' => 'Sugerencia',
'message' => "Activa ahorro automático del 10% cada vez que recibes tu nómina.",
];
}
return $insights;
}
/**
* Historial de puntuación
*/
public function history(Request $request)
{
// Por ahora retornamos datos simulados
// En producción, guardaríamos snapshots diarios/semanales
$months = $request->get('months', 6);
$history = [];
$baseScore = 72;
for ($i = $months - 1; $i >= 0; $i--) {
$date = now()->subMonths($i);
$variation = rand(-5, 8);
$baseScore = max(30, min(95, $baseScore + $variation));
$history[] = [
'month' => $date->format('Y-m'),
'month_label' => $date->format('M Y'),
'score' => $baseScore,
];
}
return response()->json($history);
}
}