- FinancialHealthController: Fix column name in queries - ReportController: Fix column name in queries - Budget model: Fix getSpentAmountAttribute query
499 lines
19 KiB
PHP
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);
|
|
}
|
|
}
|