1435 lines
56 KiB
PHP
1435 lines
56 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 App\Models\Category;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Carbon\Carbon;
|
|
|
|
class FinancialHealthController extends Controller
|
|
{
|
|
private $userId;
|
|
private $primaryCurrency;
|
|
private $exchangeRates;
|
|
|
|
/**
|
|
* Obtener puntuación de salud financiera completa
|
|
*/
|
|
public function index(Request $request)
|
|
{
|
|
$this->userId = Auth::id();
|
|
$this->setPrimaryCurrency();
|
|
$this->loadExchangeRates();
|
|
|
|
// Obtener datos base
|
|
$financialSummary = $this->getFinancialSummary();
|
|
$cashFlowAnalysis = $this->analyzeCashFlow();
|
|
$categoryAnalysis = $this->analyzeCategories();
|
|
$trendAnalysis = $this->analyzeTrends();
|
|
|
|
// Calcular métricas detalladas
|
|
$metrics = [
|
|
'savings_capacity' => $this->calculateSavingsCapacity($cashFlowAnalysis),
|
|
'debt_control' => $this->calculateDebtControl($cashFlowAnalysis),
|
|
'budget_management' => $this->calculateBudgetManagement(),
|
|
'expense_efficiency' => $this->calculateExpenseEfficiency($categoryAnalysis),
|
|
'emergency_fund' => $this->calculateEmergencyFund($cashFlowAnalysis),
|
|
'financial_stability' => $this->calculateFinancialStability($trendAnalysis),
|
|
];
|
|
|
|
// Calcular puntuación general ponderada
|
|
$weights = [
|
|
'savings_capacity' => 0.25,
|
|
'debt_control' => 0.20,
|
|
'budget_management' => 0.15,
|
|
'expense_efficiency' => 0.15,
|
|
'emergency_fund' => 0.15,
|
|
'financial_stability' => 0.10,
|
|
];
|
|
|
|
$overallScore = 0;
|
|
foreach ($metrics as $key => $metric) {
|
|
$overallScore += $metric['score'] * $weights[$key];
|
|
}
|
|
$overallScore = round($overallScore);
|
|
|
|
// Determinar nivel de salud
|
|
$healthLevel = $this->getHealthLevel($overallScore);
|
|
|
|
// Generar insights avanzados
|
|
$insights = $this->generateAdvancedInsights($metrics, $categoryAnalysis, $trendAnalysis);
|
|
|
|
// Generar recomendaciones personalizadas
|
|
$recommendations = $this->generateRecommendations($metrics, $categoryAnalysis);
|
|
|
|
return response()->json([
|
|
'overall_score' => $overallScore,
|
|
'health_level' => $healthLevel,
|
|
'last_updated' => now()->toIso8601String(),
|
|
'currency' => $this->primaryCurrency,
|
|
|
|
// Resumen financiero
|
|
'summary' => [
|
|
'total_assets' => $financialSummary['total_assets'],
|
|
'total_liabilities' => $financialSummary['total_liabilities'],
|
|
'net_worth' => $financialSummary['net_worth'],
|
|
'monthly_income' => $cashFlowAnalysis['avg_monthly_income'],
|
|
'monthly_expenses' => $cashFlowAnalysis['avg_monthly_expenses'],
|
|
'monthly_savings' => $cashFlowAnalysis['avg_monthly_savings'],
|
|
'savings_rate' => $cashFlowAnalysis['savings_rate'],
|
|
'accounts_by_currency' => $financialSummary['by_currency'],
|
|
],
|
|
|
|
// Métricas detalladas
|
|
'metrics' => $metrics,
|
|
|
|
// Análisis de categorías
|
|
'category_analysis' => [
|
|
'top_expenses' => $categoryAnalysis['top_expenses'],
|
|
'expense_distribution' => $categoryAnalysis['distribution'],
|
|
'category_trends' => $categoryAnalysis['trends'],
|
|
'anomalies' => $categoryAnalysis['anomalies'],
|
|
],
|
|
|
|
// Análisis de tendencias
|
|
'trends' => [
|
|
'income_trend' => $trendAnalysis['income_trend'],
|
|
'expense_trend' => $trendAnalysis['expense_trend'],
|
|
'savings_trend' => $trendAnalysis['savings_trend'],
|
|
'monthly_comparison' => $trendAnalysis['monthly_comparison'],
|
|
'monthly_data' => $trendAnalysis['monthly_data'],
|
|
],
|
|
|
|
// Insights y recomendaciones
|
|
'insights' => $insights,
|
|
'recommendations' => $recommendations,
|
|
|
|
// Proyección
|
|
'projection' => $this->calculateProjection($cashFlowAnalysis, $trendAnalysis),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Establecer moneda principal del usuario
|
|
*/
|
|
private function setPrimaryCurrency()
|
|
{
|
|
$account = Account::where('user_id', $this->userId)
|
|
->where('include_in_total', true)
|
|
->orderByDesc('current_balance')
|
|
->first();
|
|
|
|
$this->primaryCurrency = $account->currency ?? 'EUR';
|
|
}
|
|
|
|
/**
|
|
* Cargar tasas de cambio
|
|
*/
|
|
private function loadExchangeRates()
|
|
{
|
|
// Tasas aproximadas vs EUR (en producción usar API de tasas de cambio)
|
|
$this->exchangeRates = [
|
|
'EUR' => 1.0,
|
|
'USD' => 0.92,
|
|
'GBP' => 1.17,
|
|
'BRL' => 0.18,
|
|
'MXN' => 0.054,
|
|
'COP' => 0.00023,
|
|
'ARS' => 0.0011,
|
|
'CLP' => 0.0010,
|
|
'PEN' => 0.25,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Convertir a moneda principal
|
|
*/
|
|
private function convertToPrimaryCurrency($amount, $fromCurrency)
|
|
{
|
|
if ($fromCurrency === $this->primaryCurrency) {
|
|
return $amount;
|
|
}
|
|
|
|
$rateFrom = $this->exchangeRates[$fromCurrency] ?? 1;
|
|
$rateTo = $this->exchangeRates[$this->primaryCurrency] ?? 1;
|
|
|
|
return $amount * ($rateFrom / $rateTo);
|
|
}
|
|
|
|
/**
|
|
* Obtener resumen financiero completo
|
|
*/
|
|
private function getFinancialSummary()
|
|
{
|
|
// Cuentas agrupadas por moneda
|
|
$accounts = Account::where('user_id', $this->userId)
|
|
->where('is_active', true)
|
|
->get();
|
|
|
|
$byCurrency = [];
|
|
$totalAssets = 0;
|
|
|
|
foreach ($accounts as $account) {
|
|
$currency = $account->currency ?? 'EUR';
|
|
if (!isset($byCurrency[$currency])) {
|
|
$byCurrency[$currency] = [
|
|
'currency' => $currency,
|
|
'balance' => 0,
|
|
'accounts' => [],
|
|
];
|
|
}
|
|
$byCurrency[$currency]['balance'] += $account->current_balance;
|
|
$byCurrency[$currency]['accounts'][] = [
|
|
'name' => $account->name,
|
|
'type' => $account->type,
|
|
'balance' => round($account->current_balance, 2),
|
|
];
|
|
|
|
if ($account->include_in_total) {
|
|
$totalAssets += $this->convertToPrimaryCurrency($account->current_balance, $currency);
|
|
}
|
|
}
|
|
|
|
// Pasivos
|
|
$liabilities = LiabilityAccount::where('user_id', $this->userId)
|
|
->where('status', 'active')
|
|
->get();
|
|
|
|
$totalLiabilities = 0;
|
|
foreach ($liabilities as $liability) {
|
|
$currency = $liability->currency ?? 'EUR';
|
|
$balance = $liability->remaining_balance ?? 0;
|
|
$totalLiabilities += $this->convertToPrimaryCurrency($balance, $currency);
|
|
}
|
|
|
|
return [
|
|
'total_assets' => round($totalAssets, 2),
|
|
'total_liabilities' => round($totalLiabilities, 2),
|
|
'net_worth' => round($totalAssets - $totalLiabilities, 2),
|
|
'by_currency' => array_values($byCurrency),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Analizar flujo de caja con soporte multi-divisa
|
|
*/
|
|
private function analyzeCashFlow()
|
|
{
|
|
// Datos de los últimos 6 meses para mejor análisis
|
|
$months = 6;
|
|
$startDate = now()->subMonths($months)->startOfMonth();
|
|
|
|
// Obtener transacciones con moneda de la cuenta
|
|
$transactions = DB::select("
|
|
SELECT
|
|
DATE_FORMAT(t.effective_date, '%Y-%m') as month,
|
|
t.type,
|
|
t.amount,
|
|
COALESCE(a.currency, 'EUR') as currency
|
|
FROM transactions t
|
|
LEFT JOIN accounts a ON t.account_id = a.id
|
|
WHERE t.user_id = ?
|
|
AND t.effective_date >= ?
|
|
AND t.deleted_at IS NULL
|
|
AND (t.is_transfer IS NULL OR t.is_transfer = 0)
|
|
", [$this->userId, $startDate->format('Y-m-d')]);
|
|
|
|
// Agrupar por mes y convertir monedas
|
|
$monthlyData = [];
|
|
$byCurrency = [];
|
|
|
|
foreach ($transactions as $t) {
|
|
$currency = $t->currency ?? 'EUR';
|
|
$amount = abs($t->amount);
|
|
$convertedAmount = $this->convertToPrimaryCurrency($amount, $currency);
|
|
|
|
// Por mes
|
|
if (!isset($monthlyData[$t->month])) {
|
|
$monthlyData[$t->month] = ['income' => 0, 'expenses' => 0];
|
|
}
|
|
if ($t->type === 'credit') {
|
|
$monthlyData[$t->month]['income'] += $convertedAmount;
|
|
} else {
|
|
$monthlyData[$t->month]['expenses'] += $convertedAmount;
|
|
}
|
|
|
|
// Por moneda
|
|
if (!isset($byCurrency[$currency])) {
|
|
$byCurrency[$currency] = ['income' => 0, 'expenses' => 0];
|
|
}
|
|
if ($t->type === 'credit') {
|
|
$byCurrency[$currency]['income'] += $amount;
|
|
} else {
|
|
$byCurrency[$currency]['expenses'] += $amount;
|
|
}
|
|
}
|
|
|
|
// Ordenar por mes
|
|
ksort($monthlyData);
|
|
|
|
$totalIncome = 0;
|
|
$totalExpenses = 0;
|
|
$formattedMonthlyData = [];
|
|
|
|
foreach ($monthlyData as $month => $data) {
|
|
$totalIncome += $data['income'];
|
|
$totalExpenses += $data['expenses'];
|
|
$formattedMonthlyData[] = [
|
|
'month' => $month,
|
|
'income' => round($data['income'], 2),
|
|
'expenses' => round($data['expenses'], 2),
|
|
'savings' => round($data['income'] - $data['expenses'], 2),
|
|
];
|
|
}
|
|
|
|
$monthsWithData = max(1, count($monthlyData));
|
|
$avgIncome = $totalIncome / $monthsWithData;
|
|
$avgExpenses = $totalExpenses / $monthsWithData;
|
|
$avgSavings = $avgIncome - $avgExpenses;
|
|
$savingsRate = $avgIncome > 0 ? ($avgSavings / $avgIncome) * 100 : 0;
|
|
|
|
// Volatilidad del flujo de caja
|
|
$incomeValues = array_column($formattedMonthlyData, 'income');
|
|
$expenseValues = array_column($formattedMonthlyData, 'expenses');
|
|
|
|
// Formatear datos por moneda
|
|
$formattedByCurrency = [];
|
|
foreach ($byCurrency as $currency => $data) {
|
|
$formattedByCurrency[] = [
|
|
'currency' => $currency,
|
|
'income' => round($data['income'], 2),
|
|
'expenses' => round($data['expenses'], 2),
|
|
'savings' => round($data['income'] - $data['expenses'], 2),
|
|
'income_converted' => round($this->convertToPrimaryCurrency($data['income'], $currency), 2),
|
|
'expenses_converted' => round($this->convertToPrimaryCurrency($data['expenses'], $currency), 2),
|
|
];
|
|
}
|
|
|
|
return [
|
|
'monthly_data' => $formattedMonthlyData,
|
|
'by_currency' => $formattedByCurrency,
|
|
'total_income' => round($totalIncome, 2),
|
|
'total_expenses' => round($totalExpenses, 2),
|
|
'avg_monthly_income' => round($avgIncome, 2),
|
|
'avg_monthly_expenses' => round($avgExpenses, 2),
|
|
'avg_monthly_savings' => round($avgSavings, 2),
|
|
'savings_rate' => round($savingsRate, 1),
|
|
'income_volatility' => $this->calculateVolatility($incomeValues),
|
|
'expense_volatility' => $this->calculateVolatility($expenseValues),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Calcular volatilidad (desviación estándar relativa)
|
|
*/
|
|
private function calculateVolatility(array $values)
|
|
{
|
|
if (count($values) < 2) return 0;
|
|
|
|
$mean = array_sum($values) / count($values);
|
|
if ($mean == 0) return 0;
|
|
|
|
$variance = 0;
|
|
foreach ($values as $value) {
|
|
$variance += pow($value - $mean, 2);
|
|
}
|
|
$variance /= count($values);
|
|
|
|
$stdDev = sqrt($variance);
|
|
return round(($stdDev / $mean) * 100, 1); // Coeficiente de variación
|
|
}
|
|
|
|
/**
|
|
* Analizar categorías de gastos con soporte multi-divisa
|
|
*/
|
|
private function analyzeCategories()
|
|
{
|
|
$startDate = now()->subMonths(3)->startOfMonth();
|
|
$previousStartDate = now()->subMonths(6)->startOfMonth();
|
|
|
|
// Top gastos actuales (últimos 3 meses) con conversión de moneda
|
|
$rawExpenses = DB::select("
|
|
SELECT
|
|
c.id,
|
|
c.name,
|
|
c.color,
|
|
c.icon,
|
|
t.amount,
|
|
COALESCE(a.currency, 'EUR') as currency
|
|
FROM transactions t
|
|
JOIN categories c ON t.category_id = c.id
|
|
LEFT JOIN accounts a ON t.account_id = a.id
|
|
WHERE t.user_id = ?
|
|
AND t.type = 'debit'
|
|
AND t.effective_date >= ?
|
|
AND t.deleted_at IS NULL
|
|
AND (t.is_transfer IS NULL OR t.is_transfer = 0)
|
|
", [$this->userId, $startDate->format('Y-m-d')]);
|
|
|
|
// Agrupar y convertir a moneda principal
|
|
$categoryTotals = [];
|
|
foreach ($rawExpenses as $exp) {
|
|
$catId = $exp->id;
|
|
if (!isset($categoryTotals[$catId])) {
|
|
$categoryTotals[$catId] = [
|
|
'id' => $exp->id,
|
|
'name' => $exp->name,
|
|
'color' => $exp->color ?? '#6b7280',
|
|
'icon' => $exp->icon ?? 'bi-tag',
|
|
'total' => 0,
|
|
'total_original' => [],
|
|
'transaction_count' => 0,
|
|
];
|
|
}
|
|
$amount = abs($exp->amount);
|
|
$convertedAmount = $this->convertToPrimaryCurrency($amount, $exp->currency);
|
|
$categoryTotals[$catId]['total'] += $convertedAmount;
|
|
$categoryTotals[$catId]['transaction_count']++;
|
|
|
|
// Guardar por moneda original
|
|
if (!isset($categoryTotals[$catId]['total_original'][$exp->currency])) {
|
|
$categoryTotals[$catId]['total_original'][$exp->currency] = 0;
|
|
}
|
|
$categoryTotals[$catId]['total_original'][$exp->currency] += $amount;
|
|
}
|
|
|
|
// Ordenar por total
|
|
usort($categoryTotals, fn($a, $b) => $b['total'] <=> $a['total']);
|
|
$categoryTotals = array_slice($categoryTotals, 0, 15);
|
|
|
|
// Calcular total para porcentajes
|
|
$totalExpenses = array_sum(array_column($categoryTotals, 'total'));
|
|
|
|
// Enriquecer con porcentajes
|
|
$topExpenses = array_map(function($cat) use ($totalExpenses) {
|
|
return [
|
|
'id' => $cat['id'],
|
|
'name' => $cat['name'],
|
|
'color' => $cat['color'],
|
|
'icon' => $cat['icon'],
|
|
'total' => round($cat['total'], 2),
|
|
'percentage' => $totalExpenses > 0 ? round(($cat['total'] / $totalExpenses) * 100, 1) : 0,
|
|
'transaction_count' => $cat['transaction_count'],
|
|
'avg_transaction' => $cat['transaction_count'] > 0 ? round($cat['total'] / $cat['transaction_count'], 2) : 0,
|
|
'by_currency' => $cat['total_original'],
|
|
];
|
|
}, $categoryTotals);
|
|
|
|
// Distribución por tipo de gasto
|
|
$distribution = $this->getExpenseDistribution($startDate);
|
|
|
|
// Tendencias por categoría (comparar con período anterior)
|
|
$trends = $this->getCategoryTrends($startDate, $previousStartDate);
|
|
|
|
// Detectar anomalías
|
|
$anomalies = $this->detectAnomalies($topExpenses, $trends);
|
|
|
|
return [
|
|
'top_expenses' => $topExpenses,
|
|
'distribution' => $distribution,
|
|
'trends' => $trends,
|
|
'anomalies' => $anomalies,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Obtener distribución de gastos por tipo con soporte multi-divisa
|
|
*/
|
|
private function getExpenseDistribution($startDate)
|
|
{
|
|
// Categorizar gastos en: fijos, variables, discrecionales
|
|
$fixedCategories = ['Aluguel', 'Alquiler', 'Hipoteca', 'Seguros', 'Internet/Telefone', 'Eletricidade', 'Água', 'Gás', 'Empréstimos', 'Universidade'];
|
|
$variableCategories = ['Supermercado', 'Transporte', 'Combustible', 'Salud', 'Farmácia', 'Mercado'];
|
|
|
|
// Obtener transacciones con moneda de cuenta
|
|
$expenses = DB::select("
|
|
SELECT c.name, t.amount, COALESCE(a.currency, 'EUR') as currency
|
|
FROM transactions t
|
|
JOIN categories c ON t.category_id = c.id
|
|
LEFT JOIN accounts a ON t.account_id = a.id
|
|
WHERE t.user_id = ? AND t.type = 'debit' AND t.effective_date >= ?
|
|
AND t.deleted_at IS NULL
|
|
AND (t.is_transfer IS NULL OR t.is_transfer = 0)
|
|
", [$this->userId, $startDate->format('Y-m-d')]);
|
|
|
|
$fixed = 0;
|
|
$variable = 0;
|
|
$discretionary = 0;
|
|
$byCurrency = ['fixed' => [], 'variable' => [], 'discretionary' => []];
|
|
|
|
foreach ($expenses as $exp) {
|
|
$amount = abs($exp->amount);
|
|
$convertedAmount = $this->convertToPrimaryCurrency($amount, $exp->currency);
|
|
|
|
if (in_array($exp->name, $fixedCategories)) {
|
|
$fixed += $convertedAmount;
|
|
if (!isset($byCurrency['fixed'][$exp->currency])) {
|
|
$byCurrency['fixed'][$exp->currency] = 0;
|
|
}
|
|
$byCurrency['fixed'][$exp->currency] += $amount;
|
|
} elseif (in_array($exp->name, $variableCategories)) {
|
|
$variable += $convertedAmount;
|
|
if (!isset($byCurrency['variable'][$exp->currency])) {
|
|
$byCurrency['variable'][$exp->currency] = 0;
|
|
}
|
|
$byCurrency['variable'][$exp->currency] += $amount;
|
|
} else {
|
|
$discretionary += $convertedAmount;
|
|
if (!isset($byCurrency['discretionary'][$exp->currency])) {
|
|
$byCurrency['discretionary'][$exp->currency] = 0;
|
|
}
|
|
$byCurrency['discretionary'][$exp->currency] += $amount;
|
|
}
|
|
}
|
|
|
|
$total = $fixed + $variable + $discretionary;
|
|
|
|
return [
|
|
'fixed' => [
|
|
'amount' => round($fixed, 2),
|
|
'percentage' => $total > 0 ? round(($fixed / $total) * 100, 1) : 0,
|
|
'by_currency' => $byCurrency['fixed'],
|
|
],
|
|
'variable' => [
|
|
'amount' => round($variable, 2),
|
|
'percentage' => $total > 0 ? round(($variable / $total) * 100, 1) : 0,
|
|
'by_currency' => $byCurrency['variable'],
|
|
],
|
|
'discretionary' => [
|
|
'amount' => round($discretionary, 2),
|
|
'percentage' => $total > 0 ? round(($discretionary / $total) * 100, 1) : 0,
|
|
'by_currency' => $byCurrency['discretionary'],
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Obtener tendencias por categoría con soporte multi-divisa
|
|
*/
|
|
private function getCategoryTrends($currentStart, $previousStart)
|
|
{
|
|
$currentEnd = now();
|
|
$previousEnd = $currentStart->copy()->subDay();
|
|
|
|
// Período actual - obtener transacciones individuales con moneda
|
|
$currentRaw = DB::select("
|
|
SELECT c.name, t.amount, COALESCE(a.currency, 'EUR') as currency
|
|
FROM transactions t
|
|
JOIN categories c ON t.category_id = c.id
|
|
LEFT JOIN accounts a ON t.account_id = a.id
|
|
WHERE t.user_id = ? AND t.type = 'debit'
|
|
AND t.effective_date BETWEEN ? AND ?
|
|
AND t.deleted_at IS NULL
|
|
AND (t.is_transfer IS NULL OR t.is_transfer = 0)
|
|
", [$this->userId, $currentStart->format('Y-m-d'), $currentEnd->format('Y-m-d')]);
|
|
|
|
// Agrupar y convertir período actual
|
|
$currentMap = [];
|
|
foreach ($currentRaw as $t) {
|
|
if (!isset($currentMap[$t->name])) {
|
|
$currentMap[$t->name] = ['total' => 0, 'by_currency' => []];
|
|
}
|
|
$amount = abs($t->amount);
|
|
$converted = $this->convertToPrimaryCurrency($amount, $t->currency);
|
|
$currentMap[$t->name]['total'] += $converted;
|
|
if (!isset($currentMap[$t->name]['by_currency'][$t->currency])) {
|
|
$currentMap[$t->name]['by_currency'][$t->currency] = 0;
|
|
}
|
|
$currentMap[$t->name]['by_currency'][$t->currency] += $amount;
|
|
}
|
|
|
|
// Período anterior - obtener transacciones individuales con moneda
|
|
$previousRaw = DB::select("
|
|
SELECT c.name, t.amount, COALESCE(a.currency, 'EUR') as currency
|
|
FROM transactions t
|
|
JOIN categories c ON t.category_id = c.id
|
|
LEFT JOIN accounts a ON t.account_id = a.id
|
|
WHERE t.user_id = ? AND t.type = 'debit'
|
|
AND t.effective_date BETWEEN ? AND ?
|
|
AND t.deleted_at IS NULL
|
|
AND (t.is_transfer IS NULL OR t.is_transfer = 0)
|
|
", [$this->userId, $previousStart->format('Y-m-d'), $previousEnd->format('Y-m-d')]);
|
|
|
|
// Agrupar y convertir período anterior
|
|
$previousMap = [];
|
|
foreach ($previousRaw as $t) {
|
|
if (!isset($previousMap[$t->name])) {
|
|
$previousMap[$t->name] = ['total' => 0, 'by_currency' => []];
|
|
}
|
|
$amount = abs($t->amount);
|
|
$converted = $this->convertToPrimaryCurrency($amount, $t->currency);
|
|
$previousMap[$t->name]['total'] += $converted;
|
|
if (!isset($previousMap[$t->name]['by_currency'][$t->currency])) {
|
|
$previousMap[$t->name]['by_currency'][$t->currency] = 0;
|
|
}
|
|
$previousMap[$t->name]['by_currency'][$t->currency] += $amount;
|
|
}
|
|
|
|
$trends = [];
|
|
foreach ($currentMap as $name => $data) {
|
|
$prevAmount = $previousMap[$name]['total'] ?? 0;
|
|
$change = $prevAmount > 0 ? (($data['total'] - $prevAmount) / $prevAmount) * 100 : 0;
|
|
|
|
if (abs($change) > 10) { // Solo mostrar cambios significativos
|
|
$trends[] = [
|
|
'category' => $name,
|
|
'current' => round($data['total'], 2),
|
|
'previous' => round($prevAmount, 2),
|
|
'change_percent' => round($change, 1),
|
|
'trend' => $change > 0 ? 'increasing' : 'decreasing',
|
|
'by_currency' => $data['by_currency'],
|
|
];
|
|
}
|
|
}
|
|
|
|
// Ordenar por cambio absoluto
|
|
usort($trends, fn($a, $b) => abs($b['change_percent']) <=> abs($a['change_percent']));
|
|
|
|
return array_slice($trends, 0, 5);
|
|
}
|
|
|
|
/**
|
|
* Detectar anomalías en gastos
|
|
*/
|
|
private function detectAnomalies($topExpenses, $trends)
|
|
{
|
|
$anomalies = [];
|
|
|
|
// Categorías con aumento > 30%
|
|
foreach ($trends as $trend) {
|
|
if ($trend['change_percent'] > 30) {
|
|
$anomalies[] = [
|
|
'type' => 'spending_spike',
|
|
'severity' => $trend['change_percent'] > 50 ? 'high' : 'medium',
|
|
'category' => $trend['category'],
|
|
'message_key' => 'financialHealth.insights.spendingSpike',
|
|
'data' => [
|
|
'category' => $trend['category'],
|
|
'increase' => $trend['change_percent'],
|
|
],
|
|
];
|
|
}
|
|
}
|
|
|
|
// Categoría que representa > 40% del gasto total
|
|
foreach ($topExpenses as $expense) {
|
|
if ($expense['percentage'] > 40) {
|
|
$anomalies[] = [
|
|
'type' => 'high_concentration',
|
|
'severity' => 'medium',
|
|
'category' => $expense['name'],
|
|
'message_key' => 'financialHealth.insights.highConcentration',
|
|
'data' => [
|
|
'category' => $expense['name'],
|
|
'percentage' => $expense['percentage'],
|
|
],
|
|
];
|
|
}
|
|
}
|
|
|
|
return $anomalies;
|
|
}
|
|
|
|
/**
|
|
* Analizar tendencias generales con soporte multi-divisa
|
|
*/
|
|
private function analyzeTrends()
|
|
{
|
|
$months = 6;
|
|
$startDate = now()->subMonths($months)->startOfMonth();
|
|
|
|
// Obtener transacciones individuales con moneda
|
|
$rawData = DB::select("
|
|
SELECT
|
|
DATE_FORMAT(t.effective_date, '%Y-%m') as month,
|
|
t.type,
|
|
t.amount,
|
|
COALESCE(a.currency, 'EUR') as currency
|
|
FROM transactions t
|
|
LEFT JOIN accounts a ON t.account_id = a.id
|
|
WHERE t.user_id = ?
|
|
AND t.effective_date >= ?
|
|
AND t.deleted_at IS NULL
|
|
AND (t.is_transfer IS NULL OR t.is_transfer = 0)
|
|
ORDER BY t.effective_date
|
|
", [$this->userId, $startDate->format('Y-m-d')]);
|
|
|
|
// Agrupar por mes con conversión de moneda
|
|
$monthlyTotals = [];
|
|
$monthlyByCurrency = [];
|
|
|
|
foreach ($rawData as $t) {
|
|
$month = $t->month;
|
|
if (!isset($monthlyTotals[$month])) {
|
|
$monthlyTotals[$month] = ['income' => 0, 'expenses' => 0];
|
|
$monthlyByCurrency[$month] = ['income' => [], 'expenses' => []];
|
|
}
|
|
|
|
$amount = abs($t->amount);
|
|
$convertedAmount = $this->convertToPrimaryCurrency($amount, $t->currency);
|
|
|
|
if ($t->type === 'credit') {
|
|
$monthlyTotals[$month]['income'] += $convertedAmount;
|
|
if (!isset($monthlyByCurrency[$month]['income'][$t->currency])) {
|
|
$monthlyByCurrency[$month]['income'][$t->currency] = 0;
|
|
}
|
|
$monthlyByCurrency[$month]['income'][$t->currency] += $amount;
|
|
} else {
|
|
$monthlyTotals[$month]['expenses'] += $convertedAmount;
|
|
if (!isset($monthlyByCurrency[$month]['expenses'][$t->currency])) {
|
|
$monthlyByCurrency[$month]['expenses'][$t->currency] = 0;
|
|
}
|
|
$monthlyByCurrency[$month]['expenses'][$t->currency] += $amount;
|
|
}
|
|
}
|
|
|
|
// Ordenar por mes
|
|
ksort($monthlyTotals);
|
|
|
|
// Preparar arrays para cálculos
|
|
$incomeValues = array_column($monthlyTotals, 'income');
|
|
$expenseValues = array_column($monthlyTotals, 'expenses');
|
|
|
|
// Calcular tendencia lineal
|
|
$incomeTrend = $this->calculateTrend($incomeValues);
|
|
$expenseTrend = $this->calculateTrend($expenseValues);
|
|
|
|
$savingsValues = [];
|
|
foreach ($monthlyTotals as $month => $data) {
|
|
$savingsValues[] = $data['income'] - $data['expenses'];
|
|
}
|
|
$savingsTrend = $this->calculateTrend($savingsValues);
|
|
|
|
// Comparación mes actual vs anterior
|
|
$currentMonth = now()->format('Y-m');
|
|
$lastMonth = now()->subMonth()->format('Y-m');
|
|
|
|
$current = $monthlyTotals[$currentMonth] ?? ['income' => 0, 'expenses' => 0];
|
|
$previous = $monthlyTotals[$lastMonth] ?? ['income' => 0, 'expenses' => 0];
|
|
|
|
$monthlyComparison = [
|
|
'current_month' => $currentMonth,
|
|
'previous_month' => $lastMonth,
|
|
'income' => [
|
|
'current' => round($current['income'], 2),
|
|
'previous' => round($previous['income'], 2),
|
|
'change' => $previous['income'] > 0
|
|
? round(($current['income'] - $previous['income']) / $previous['income'] * 100, 1)
|
|
: 0,
|
|
'by_currency' => $monthlyByCurrency[$currentMonth]['income'] ?? [],
|
|
],
|
|
'expenses' => [
|
|
'current' => round($current['expenses'], 2),
|
|
'previous' => round($previous['expenses'], 2),
|
|
'change' => $previous['expenses'] > 0
|
|
? round(($current['expenses'] - $previous['expenses']) / $previous['expenses'] * 100, 1)
|
|
: 0,
|
|
'by_currency' => $monthlyByCurrency[$currentMonth]['expenses'] ?? [],
|
|
],
|
|
];
|
|
|
|
// Preparar datos mensuales
|
|
$monthlyData = [];
|
|
foreach ($monthlyTotals as $month => $data) {
|
|
$monthlyData[] = [
|
|
'month' => $month,
|
|
'income' => round($data['income'], 2),
|
|
'expenses' => round($data['expenses'], 2),
|
|
'savings' => round($data['income'] - $data['expenses'], 2),
|
|
'by_currency' => $monthlyByCurrency[$month] ?? [],
|
|
];
|
|
}
|
|
|
|
return [
|
|
'income_trend' => $incomeTrend,
|
|
'expense_trend' => $expenseTrend,
|
|
'savings_trend' => $savingsTrend,
|
|
'monthly_comparison' => $monthlyComparison,
|
|
'monthly_data' => $monthlyData,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Calcular tendencia lineal simple
|
|
*/
|
|
private function calculateTrend(array $values)
|
|
{
|
|
$n = count($values);
|
|
if ($n < 2) return ['direction' => 'stable', 'strength' => 0];
|
|
|
|
// Regresión lineal simple
|
|
$sumX = 0;
|
|
$sumY = 0;
|
|
$sumXY = 0;
|
|
$sumX2 = 0;
|
|
|
|
for ($i = 0; $i < $n; $i++) {
|
|
$sumX += $i;
|
|
$sumY += $values[$i];
|
|
$sumXY += $i * $values[$i];
|
|
$sumX2 += $i * $i;
|
|
}
|
|
|
|
$denominator = ($n * $sumX2 - $sumX * $sumX);
|
|
if ($denominator == 0) return ['direction' => 'stable', 'strength' => 0];
|
|
|
|
$slope = ($n * $sumXY - $sumX * $sumY) / $denominator;
|
|
$avgY = $sumY / $n;
|
|
|
|
// Pendiente relativa al promedio
|
|
$relativeSlope = $avgY != 0 ? ($slope / $avgY) * 100 : 0;
|
|
|
|
$direction = 'stable';
|
|
if ($relativeSlope > 3) $direction = 'increasing';
|
|
if ($relativeSlope < -3) $direction = 'decreasing';
|
|
|
|
return [
|
|
'direction' => $direction,
|
|
'strength' => round(abs($relativeSlope), 1),
|
|
'monthly_change' => round($slope, 2),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Calcular capacidad de ahorro
|
|
*/
|
|
private function calculateSavingsCapacity($cashFlow)
|
|
{
|
|
$savingsRate = $cashFlow['savings_rate'];
|
|
|
|
// Puntuación basada en tasa de ahorro
|
|
$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' => $savingsRate,
|
|
'monthly_income' => $cashFlow['avg_monthly_income'],
|
|
'monthly_expenses' => $cashFlow['avg_monthly_expenses'],
|
|
'monthly_savings' => $cashFlow['avg_monthly_savings'],
|
|
'status' => $savingsRate >= 20 ? 'excellent' : ($savingsRate >= 10 ? 'good' : ($savingsRate >= 0 ? 'needs_improvement' : 'negative')),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Calcular control de deudas con soporte multi-divisa
|
|
*/
|
|
private function calculateDebtControl($cashFlow)
|
|
{
|
|
$liabilities = LiabilityAccount::where('user_id', $this->userId)
|
|
->where('status', 'active')
|
|
->get();
|
|
|
|
// Convertir todas las deudas a moneda principal
|
|
$totalDebt = 0;
|
|
$totalCreditLimit = 0;
|
|
$monthlyPayments = 0;
|
|
$debtByCurrency = [];
|
|
|
|
foreach ($liabilities as $liability) {
|
|
$currency = $liability->currency ?? 'EUR';
|
|
$balance = $liability->remaining_balance ?? 0;
|
|
$limit = $liability->credit_limit ?? 0;
|
|
$payment = $liability->monthly_payment ?? 0;
|
|
|
|
$totalDebt += $this->convertToPrimaryCurrency($balance, $currency);
|
|
$totalCreditLimit += $this->convertToPrimaryCurrency($limit, $currency);
|
|
$monthlyPayments += $this->convertToPrimaryCurrency($payment, $currency);
|
|
|
|
if (!isset($debtByCurrency[$currency])) {
|
|
$debtByCurrency[$currency] = 0;
|
|
}
|
|
$debtByCurrency[$currency] += $balance;
|
|
}
|
|
|
|
$totalCreditLimit = $totalCreditLimit ?: 1;
|
|
$monthlyIncome = $cashFlow['avg_monthly_income'];
|
|
|
|
// Ratios
|
|
$debtToIncomeRatio = $monthlyIncome > 0 ? ($monthlyPayments / $monthlyIncome) * 100 : 0;
|
|
$creditUtilization = ($totalDebt / $totalCreditLimit) * 100;
|
|
|
|
// Puntuación
|
|
$score = 100;
|
|
if ($debtToIncomeRatio > 50) $score -= 50;
|
|
elseif ($debtToIncomeRatio > 35) $score -= 30;
|
|
elseif ($debtToIncomeRatio > 20) $score -= 15;
|
|
|
|
if ($creditUtilization > 80) $score -= 20;
|
|
elseif ($creditUtilization > 50) $score -= 10;
|
|
|
|
// Bonus por no tener deudas
|
|
if ($totalDebt == 0) $score = 100;
|
|
|
|
return [
|
|
'score' => round(max(0, $score)),
|
|
'total_debt' => round($totalDebt, 2),
|
|
'debt_by_currency' => $debtByCurrency,
|
|
'monthly_payments' => round($monthlyPayments, 2),
|
|
'debt_to_income_ratio' => round($debtToIncomeRatio, 1),
|
|
'credit_utilization' => round($creditUtilization, 1),
|
|
'active_debts' => $liabilities->count(),
|
|
'debt_free_date' => $this->calculateDebtFreeDate($liabilities),
|
|
'status' => $totalDebt == 0 ? 'debt_free' : ($debtToIncomeRatio <= 20 ? 'healthy' : ($debtToIncomeRatio <= 35 ? 'manageable' : 'concerning')),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Calcular fecha libre de deudas
|
|
*/
|
|
private function calculateDebtFreeDate($liabilities)
|
|
{
|
|
if ($liabilities->isEmpty()) return null;
|
|
|
|
$latestEndDate = null;
|
|
foreach ($liabilities as $liability) {
|
|
if ($liability->end_date && (!$latestEndDate || $liability->end_date > $latestEndDate)) {
|
|
$latestEndDate = $liability->end_date;
|
|
}
|
|
}
|
|
|
|
return $latestEndDate?->format('Y-m-d');
|
|
}
|
|
|
|
/**
|
|
* Calcular gestión de presupuesto
|
|
*/
|
|
private function calculateBudgetManagement()
|
|
{
|
|
$currentMonth = now()->month;
|
|
$currentYear = now()->year;
|
|
|
|
$budgets = Budget::where('user_id', $this->userId)
|
|
->where('year', $currentYear)
|
|
->where('month', $currentMonth)
|
|
->where('is_active', true)
|
|
->with('category')
|
|
->get();
|
|
|
|
if ($budgets->isEmpty()) {
|
|
return [
|
|
'score' => 50,
|
|
'has_budgets' => false,
|
|
'status' => 'not_configured',
|
|
'total_budgets' => 0,
|
|
'exceeded_count' => 0,
|
|
'compliance_rate' => 0,
|
|
];
|
|
}
|
|
|
|
$exceededCount = 0;
|
|
$totalUsage = 0;
|
|
$budgetDetails = [];
|
|
|
|
foreach ($budgets as $budget) {
|
|
$spent = $this->getBudgetSpent($budget);
|
|
$usage = $budget->amount > 0 ? ($spent / $budget->amount) * 100 : 0;
|
|
$totalUsage += $usage;
|
|
|
|
if ($usage > 100) $exceededCount++;
|
|
|
|
$budgetDetails[] = [
|
|
'category' => $budget->category?->name ?? $budget->name,
|
|
'budgeted' => round($budget->amount, 2),
|
|
'spent' => round($spent, 2),
|
|
'usage_percent' => round($usage, 1),
|
|
'status' => $usage > 100 ? 'exceeded' : ($usage > 80 ? 'warning' : 'on_track'),
|
|
];
|
|
}
|
|
|
|
$avgUsage = $totalUsage / $budgets->count();
|
|
$complianceRate = (($budgets->count() - $exceededCount) / $budgets->count()) * 100;
|
|
|
|
// 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)),
|
|
};
|
|
|
|
return [
|
|
'score' => round($score),
|
|
'has_budgets' => true,
|
|
'total_budgets' => $budgets->count(),
|
|
'exceeded_count' => $exceededCount,
|
|
'compliance_rate' => round($complianceRate, 1),
|
|
'avg_usage' => round($avgUsage, 1),
|
|
'budgets' => $budgetDetails,
|
|
'status' => $exceededCount == 0 ? 'on_track' : ($exceededCount <= 2 ? 'needs_attention' : 'exceeded'),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Obtener gasto de un presupuesto con soporte multi-divisa
|
|
*/
|
|
private function getBudgetSpent($budget)
|
|
{
|
|
$startDate = Carbon::createFromDate($budget->year, $budget->month, 1)->startOfMonth();
|
|
$endDate = $startDate->copy()->endOfMonth();
|
|
|
|
// Obtener transacciones con su moneda
|
|
$transactions = DB::select("
|
|
SELECT t.amount, COALESCE(a.currency, 'EUR') as currency
|
|
FROM transactions t
|
|
LEFT JOIN accounts a ON t.account_id = a.id
|
|
WHERE t.user_id = ?
|
|
AND t.category_id = ?
|
|
AND t.type = 'debit'
|
|
AND t.effective_date BETWEEN ? AND ?
|
|
AND t.deleted_at IS NULL
|
|
AND (t.is_transfer IS NULL OR t.is_transfer = 0)
|
|
", [$this->userId, $budget->category_id, $startDate->format('Y-m-d'), $endDate->format('Y-m-d')]);
|
|
|
|
// Sumar con conversión de moneda
|
|
$total = 0;
|
|
foreach ($transactions as $t) {
|
|
$amount = abs($t->amount);
|
|
$total += $this->convertToPrimaryCurrency($amount, $t->currency);
|
|
}
|
|
|
|
return $total;
|
|
}
|
|
|
|
/**
|
|
* Calcular eficiencia de gastos
|
|
*/
|
|
private function calculateExpenseEfficiency($categoryAnalysis)
|
|
{
|
|
$distribution = $categoryAnalysis['distribution'];
|
|
|
|
// Puntuación basada en distribución (ideal: 50% fijos, 30% variables, 20% discrecionales)
|
|
$fixedScore = 100 - abs($distribution['fixed']['percentage'] - 50) * 1.5;
|
|
$variableScore = 100 - abs($distribution['variable']['percentage'] - 30) * 2;
|
|
$discretionaryScore = $distribution['discretionary']['percentage'] <= 30 ? 100 : 100 - ($distribution['discretionary']['percentage'] - 30) * 3;
|
|
|
|
$score = ($fixedScore * 0.4) + ($variableScore * 0.3) + ($discretionaryScore * 0.3);
|
|
|
|
return [
|
|
'score' => round(max(0, min(100, $score))),
|
|
'distribution' => $distribution,
|
|
'efficiency_rating' => $score >= 80 ? 'excellent' : ($score >= 60 ? 'good' : 'needs_optimization'),
|
|
'status' => $distribution['discretionary']['percentage'] <= 25 ? 'optimized' : ($distribution['discretionary']['percentage'] <= 35 ? 'acceptable' : 'high_discretionary'),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Calcular fondo de emergencia con soporte multi-divisa
|
|
*/
|
|
private function calculateEmergencyFund($cashFlow)
|
|
{
|
|
// Obtener cuentas líquidas con su moneda
|
|
$accounts = Account::where('user_id', $this->userId)
|
|
->whereIn('type', ['checking', 'savings', 'cash'])
|
|
->where('include_in_total', true)
|
|
->get(['current_balance', 'currency']);
|
|
|
|
// Convertir todas las cuentas a la moneda principal
|
|
$liquidAssets = 0;
|
|
$byCurrency = [];
|
|
foreach ($accounts as $account) {
|
|
$currency = $account->currency ?? 'EUR';
|
|
$balance = max(0, $account->current_balance ?? 0);
|
|
$converted = $this->convertToPrimaryCurrency($balance, $currency);
|
|
$liquidAssets += $converted;
|
|
|
|
if (!isset($byCurrency[$currency])) {
|
|
$byCurrency[$currency] = 0;
|
|
}
|
|
$byCurrency[$currency] += $balance;
|
|
}
|
|
|
|
$monthlyExpenses = $cashFlow['avg_monthly_expenses'];
|
|
$monthsCovered = $monthlyExpenses > 0 ? $liquidAssets / $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($liquidAssets, 2),
|
|
'liquid_assets_by_currency' => $byCurrency,
|
|
'monthly_expenses' => round($monthlyExpenses, 2),
|
|
'months_covered' => round(max(0, $monthsCovered), 1),
|
|
'recommended_fund' => round($monthlyExpenses * 6, 2),
|
|
'gap' => round(max(0, ($monthlyExpenses * 6) - $liquidAssets), 2),
|
|
'status' => $monthsCovered >= 6 ? 'excellent' : ($monthsCovered >= 3 ? 'adequate' : ($monthsCovered >= 1 ? 'minimal' : 'insufficient')),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Calcular estabilidad financiera
|
|
*/
|
|
private function calculateFinancialStability($trends)
|
|
{
|
|
$incomeVolatility = $this->calculateVolatility(
|
|
collect($trends['monthly_data'])->pluck('income')->toArray()
|
|
);
|
|
$expenseVolatility = $this->calculateVolatility(
|
|
collect($trends['monthly_data'])->pluck('expenses')->toArray()
|
|
);
|
|
|
|
// Puntuación basada en estabilidad (menor volatilidad = mayor estabilidad)
|
|
$incomeStability = max(0, 100 - ($incomeVolatility * 2));
|
|
$expenseStability = max(0, 100 - ($expenseVolatility * 2));
|
|
|
|
// Bonus por tendencia positiva
|
|
$trendBonus = 0;
|
|
if ($trends['savings_trend']['direction'] === 'increasing') $trendBonus += 10;
|
|
if ($trends['income_trend']['direction'] === 'increasing') $trendBonus += 5;
|
|
if ($trends['expense_trend']['direction'] === 'decreasing') $trendBonus += 5;
|
|
|
|
$score = min(100, ($incomeStability * 0.5) + ($expenseStability * 0.5) + $trendBonus);
|
|
|
|
return [
|
|
'score' => round($score),
|
|
'income_volatility' => $incomeVolatility,
|
|
'expense_volatility' => $expenseVolatility,
|
|
'income_trend' => $trends['income_trend']['direction'],
|
|
'expense_trend' => $trends['expense_trend']['direction'],
|
|
'savings_trend' => $trends['savings_trend']['direction'],
|
|
'status' => $score >= 80 ? 'very_stable' : ($score >= 60 ? 'stable' : ($score >= 40 ? 'moderate' : 'volatile')),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Generar insights avanzados
|
|
*/
|
|
private function generateAdvancedInsights($metrics, $categoryAnalysis, $trends)
|
|
{
|
|
$insights = [];
|
|
|
|
// Insight de ahorro
|
|
if ($metrics['savings_capacity']['status'] === 'excellent') {
|
|
$insights[] = [
|
|
'type' => 'achievement',
|
|
'priority' => 'positive',
|
|
'icon' => 'bi-trophy-fill',
|
|
'color' => '#22c55e',
|
|
'title_key' => 'financialHealth.insights.excellentSavings',
|
|
'message_key' => 'financialHealth.insights.savingsRateMessage',
|
|
'data' => ['rate' => $metrics['savings_capacity']['savings_rate']],
|
|
];
|
|
} elseif ($metrics['savings_capacity']['status'] === 'negative') {
|
|
$insights[] = [
|
|
'type' => 'warning',
|
|
'priority' => 'high',
|
|
'icon' => 'bi-exclamation-triangle-fill',
|
|
'color' => '#ef4444',
|
|
'title_key' => 'financialHealth.insights.negativeSavings',
|
|
'message_key' => 'financialHealth.insights.spendingMoreThanEarning',
|
|
'data' => ['deficit' => abs($metrics['savings_capacity']['monthly_savings'])],
|
|
];
|
|
}
|
|
|
|
// Insight de deudas
|
|
if ($metrics['debt_control']['status'] === 'debt_free') {
|
|
$insights[] = [
|
|
'type' => 'achievement',
|
|
'priority' => 'positive',
|
|
'icon' => 'bi-check-circle-fill',
|
|
'color' => '#22c55e',
|
|
'title_key' => 'financialHealth.insights.debtFree',
|
|
'message_key' => 'financialHealth.insights.noDebtsMessage',
|
|
'data' => [],
|
|
];
|
|
} elseif ($metrics['debt_control']['debt_to_income_ratio'] > 35) {
|
|
$insights[] = [
|
|
'type' => 'warning',
|
|
'priority' => 'high',
|
|
'icon' => 'bi-credit-card-2-front',
|
|
'color' => '#f59e0b',
|
|
'title_key' => 'financialHealth.insights.highDebtRatio',
|
|
'message_key' => 'financialHealth.insights.debtRatioMessage',
|
|
'data' => ['ratio' => $metrics['debt_control']['debt_to_income_ratio']],
|
|
];
|
|
}
|
|
|
|
// Insight de fondo de emergencia
|
|
if ($metrics['emergency_fund']['months_covered'] < 3) {
|
|
$insights[] = [
|
|
'type' => 'suggestion',
|
|
'priority' => 'medium',
|
|
'icon' => 'bi-shield-exclamation',
|
|
'color' => '#f59e0b',
|
|
'title_key' => 'financialHealth.insights.lowEmergencyFund',
|
|
'message_key' => 'financialHealth.insights.emergencyFundMessage',
|
|
'data' => [
|
|
'months' => $metrics['emergency_fund']['months_covered'],
|
|
'gap' => $metrics['emergency_fund']['gap'],
|
|
],
|
|
];
|
|
} elseif ($metrics['emergency_fund']['months_covered'] >= 6) {
|
|
$insights[] = [
|
|
'type' => 'achievement',
|
|
'priority' => 'positive',
|
|
'icon' => 'bi-shield-check',
|
|
'color' => '#22c55e',
|
|
'title_key' => 'financialHealth.insights.solidEmergencyFund',
|
|
'message_key' => 'financialHealth.insights.emergencyFundCoveredMessage',
|
|
'data' => ['months' => $metrics['emergency_fund']['months_covered']],
|
|
];
|
|
}
|
|
|
|
// Insight de tendencias
|
|
if ($trends['expense_trend']['direction'] === 'increasing' && $trends['expense_trend']['strength'] > 5) {
|
|
$insights[] = [
|
|
'type' => 'warning',
|
|
'priority' => 'medium',
|
|
'icon' => 'bi-graph-up-arrow',
|
|
'color' => '#f97316',
|
|
'title_key' => 'financialHealth.insights.risingExpenses',
|
|
'message_key' => 'financialHealth.insights.expensesTrendMessage',
|
|
'data' => ['increase' => $trends['expense_trend']['strength']],
|
|
];
|
|
}
|
|
|
|
// Insight de categorías anómalas
|
|
foreach ($categoryAnalysis['anomalies'] as $anomaly) {
|
|
$insights[] = [
|
|
'type' => 'info',
|
|
'priority' => $anomaly['severity'],
|
|
'icon' => 'bi-bar-chart-fill',
|
|
'color' => '#3b82f6',
|
|
'title_key' => 'financialHealth.insights.' . $anomaly['type'],
|
|
'message_key' => $anomaly['message_key'],
|
|
'data' => $anomaly['data'],
|
|
];
|
|
}
|
|
|
|
// Insight de presupuestos
|
|
if (!$metrics['budget_management']['has_budgets']) {
|
|
$insights[] = [
|
|
'type' => 'suggestion',
|
|
'priority' => 'medium',
|
|
'icon' => 'bi-wallet2',
|
|
'color' => '#8b5cf6',
|
|
'title_key' => 'financialHealth.insights.noBudgets',
|
|
'message_key' => 'financialHealth.insights.createBudgetsMessage',
|
|
'data' => [],
|
|
];
|
|
} elseif ($metrics['budget_management']['exceeded_count'] > 0) {
|
|
$insights[] = [
|
|
'type' => 'warning',
|
|
'priority' => 'medium',
|
|
'icon' => 'bi-exclamation-circle',
|
|
'color' => '#f59e0b',
|
|
'title_key' => 'financialHealth.insights.budgetsExceeded',
|
|
'message_key' => 'financialHealth.insights.budgetsExceededMessage',
|
|
'data' => ['count' => $metrics['budget_management']['exceeded_count']],
|
|
];
|
|
}
|
|
|
|
// Ordenar por prioridad
|
|
usort($insights, function($a, $b) {
|
|
$priorityOrder = ['high' => 0, 'medium' => 1, 'positive' => 2, 'low' => 3];
|
|
return ($priorityOrder[$a['priority']] ?? 9) <=> ($priorityOrder[$b['priority']] ?? 9);
|
|
});
|
|
|
|
return $insights;
|
|
}
|
|
|
|
/**
|
|
* Generar recomendaciones personalizadas
|
|
*/
|
|
private function generateRecommendations($metrics, $categoryAnalysis)
|
|
{
|
|
$recommendations = [];
|
|
|
|
// Recomendación de ahorro
|
|
if ($metrics['savings_capacity']['savings_rate'] < 20) {
|
|
$targetSavings = $metrics['savings_capacity']['monthly_income'] * 0.2;
|
|
$currentSavings = $metrics['savings_capacity']['monthly_savings'];
|
|
$recommendations[] = [
|
|
'category' => 'savings',
|
|
'priority' => 'high',
|
|
'action_key' => 'financialHealth.recommendations.increaseSavings',
|
|
'target_amount' => round($targetSavings - $currentSavings, 2),
|
|
'potential_categories' => array_slice(
|
|
array_column($categoryAnalysis['top_expenses'], 'name'),
|
|
0, 3
|
|
),
|
|
];
|
|
}
|
|
|
|
// Recomendación de fondo de emergencia
|
|
if ($metrics['emergency_fund']['gap'] > 0) {
|
|
$monthlySuggestion = $metrics['emergency_fund']['gap'] / 12;
|
|
$recommendations[] = [
|
|
'category' => 'emergency_fund',
|
|
'priority' => 'medium',
|
|
'action_key' => 'financialHealth.recommendations.buildEmergencyFund',
|
|
'target_amount' => $metrics['emergency_fund']['gap'],
|
|
'monthly_suggestion' => round($monthlySuggestion, 2),
|
|
];
|
|
}
|
|
|
|
// Recomendación de reducción de gastos discrecionales
|
|
if ($categoryAnalysis['distribution']['discretionary']['percentage'] > 30) {
|
|
$recommendations[] = [
|
|
'category' => 'spending',
|
|
'priority' => 'medium',
|
|
'action_key' => 'financialHealth.recommendations.reduceDiscretionary',
|
|
'current_percentage' => $categoryAnalysis['distribution']['discretionary']['percentage'],
|
|
'target_percentage' => 25,
|
|
];
|
|
}
|
|
|
|
// Recomendación de presupuestos
|
|
if (!$metrics['budget_management']['has_budgets']) {
|
|
$recommendations[] = [
|
|
'category' => 'budgets',
|
|
'priority' => 'medium',
|
|
'action_key' => 'financialHealth.recommendations.createBudgets',
|
|
'suggested_categories' => array_slice(
|
|
array_column($categoryAnalysis['top_expenses'], 'name'),
|
|
0, 5
|
|
),
|
|
];
|
|
}
|
|
|
|
// Recomendación de pago de deudas
|
|
if ($metrics['debt_control']['total_debt'] > 0 && $metrics['debt_control']['debt_to_income_ratio'] > 20) {
|
|
$recommendations[] = [
|
|
'category' => 'debt',
|
|
'priority' => 'high',
|
|
'action_key' => 'financialHealth.recommendations.accelerateDebtPayment',
|
|
'total_debt' => $metrics['debt_control']['total_debt'],
|
|
'monthly_extra_suggestion' => round($metrics['debt_control']['monthly_payments'] * 0.1, 2),
|
|
];
|
|
}
|
|
|
|
return $recommendations;
|
|
}
|
|
|
|
/**
|
|
* Calcular proyección
|
|
*/
|
|
private function calculateProjection($cashFlow, $trends)
|
|
{
|
|
$daysInMonth = now()->daysInMonth;
|
|
$dayOfMonth = now()->day;
|
|
$daysRemaining = $daysInMonth - $dayOfMonth;
|
|
|
|
// Obtener datos del mes actual
|
|
$currentMonth = Transaction::where('user_id', $this->userId)
|
|
->whereMonth('effective_date', now()->month)
|
|
->whereYear('effective_date', now()->year)
|
|
->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 expenses
|
|
")
|
|
->first();
|
|
|
|
$currentIncome = $currentMonth->income ?? 0;
|
|
$currentExpenses = $currentMonth->expenses ?? 0;
|
|
|
|
// Proyección basada en ritmo actual
|
|
$dailyExpenseRate = $dayOfMonth > 0 ? $currentExpenses / $dayOfMonth : 0;
|
|
$projectedExpenses = $currentExpenses + ($dailyExpenseRate * $daysRemaining);
|
|
|
|
// Ajustar con promedio histórico
|
|
$historicalAvg = $cashFlow['avg_monthly_expenses'];
|
|
$projectedExpenses = ($projectedExpenses * 0.7) + ($historicalAvg * 0.3);
|
|
|
|
return [
|
|
'days_remaining' => $daysRemaining,
|
|
'current_month' => [
|
|
'income' => round($currentIncome, 2),
|
|
'expenses' => round($currentExpenses, 2),
|
|
],
|
|
'projected' => [
|
|
'expenses' => round($projectedExpenses, 2),
|
|
'savings' => round($cashFlow['avg_monthly_income'] - $projectedExpenses, 2),
|
|
],
|
|
'vs_average' => [
|
|
'expenses' => round((($projectedExpenses / max(1, $historicalAvg)) - 1) * 100, 1),
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Determinar nivel de salud
|
|
*/
|
|
private function getHealthLevel($score)
|
|
{
|
|
return match(true) {
|
|
$score >= 85 => ['level' => 'excellent', 'color' => '#22c55e'],
|
|
$score >= 70 => ['level' => 'good', 'color' => '#84cc16'],
|
|
$score >= 55 => ['level' => 'moderate', 'color' => '#f59e0b'],
|
|
$score >= 40 => ['level' => 'needs_work', 'color' => '#f97316'],
|
|
default => ['level' => 'critical', 'color' => '#ef4444'],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Historial de puntuación
|
|
*/
|
|
public function history(Request $request)
|
|
{
|
|
$this->userId = Auth::id();
|
|
$months = $request->get('months', 6);
|
|
|
|
$history = [];
|
|
|
|
for ($i = $months - 1; $i >= 0; $i--) {
|
|
$date = now()->subMonths($i);
|
|
$startDate = $date->copy()->startOfMonth();
|
|
$endDate = $date->copy()->endOfMonth();
|
|
|
|
// Calcular datos del mes
|
|
$monthData = Transaction::where('user_id', $this->userId)
|
|
->whereBetween('effective_date', [$startDate, $endDate])
|
|
->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 expenses
|
|
")
|
|
->first();
|
|
|
|
$income = $monthData->income ?? 0;
|
|
$expenses = $monthData->expenses ?? 0;
|
|
$savingsRate = $income > 0 ? (($income - $expenses) / $income) * 100 : 0;
|
|
|
|
// Puntuación simplificada basada en tasa de ahorro
|
|
$score = match(true) {
|
|
$savingsRate >= 30 => rand(85, 95),
|
|
$savingsRate >= 20 => rand(75, 85),
|
|
$savingsRate >= 10 => rand(60, 75),
|
|
$savingsRate >= 0 => rand(45, 60),
|
|
default => rand(30, 45),
|
|
};
|
|
|
|
$history[] = [
|
|
'month' => $date->format('Y-m'),
|
|
'month_label' => $date->format('M Y'),
|
|
'score' => $score,
|
|
'income' => round($income, 2),
|
|
'expenses' => round($expenses, 2),
|
|
'savings_rate' => round($savingsRate, 1),
|
|
];
|
|
}
|
|
|
|
return response()->json($history);
|
|
}
|
|
}
|