fix(reports): corrigir future-transactions e overdue - v1.32.2
- Reescrito futureTransactions() com 3 fontes de dados: * Transações pending/scheduled (usando planned_date) * Cuotas de passivos pendentes * Projeções de recorrências ativas - Adicionados helpers getNextRecurrenceDates() e advanceToNextOccurrence() - Corrigida query SQL: removida referência c.name inexistente - overdueTransactions() inclui cuotas e transações vencidas - Deploy via script oficial deploy.sh Closes: endpoints /api/reports/future-transactions e /api/reports/overdue
This commit is contained in:
parent
99be24e309
commit
1feb3354ea
22
CHANGELOG.md
22
CHANGELOG.md
@ -5,6 +5,28 @@ O formato segue [Keep a Changelog](https://keepachangelog.com/pt-BR/).
|
||||
Este projeto adota [Versionamento Semântico](https://semver.org/pt-BR/).
|
||||
|
||||
|
||||
## [1.32.2] - 2025-12-14
|
||||
|
||||
### Fixed
|
||||
- **Endpoint `/api/reports/future-transactions`** - Corregido error 500 y datos vacíos
|
||||
- Reescrito completamente para incluir 3 fuentes de datos:
|
||||
1. Transacciones pendientes/programadas (usando `planned_date` en lugar de `effective_date`)
|
||||
2. Cuotas de pasivos pendientes (`liability_installments`)
|
||||
3. Proyecciones de recurrencias activas (`recurring_templates`)
|
||||
- Añadidos helpers `getNextRecurrenceDates()` y `advanceToNextOccurrence()` para calcular fechas futuras
|
||||
- Corregida query SQL: removida referencia a columna `c.name` inexistente en tabla de cuotas
|
||||
- Soporta frecuencias: diaria, semanal, quincenal, mensual, bimestral, trimestral, semestral, anual
|
||||
|
||||
- **Endpoint `/api/reports/overdue`** - Corregido para mostrar todos los vencidos
|
||||
- Incluye cuotas de pasivos vencidas (due_date < hoy)
|
||||
- Incluye transacciones pending/scheduled con fecha pasada
|
||||
- Calcula días de atraso correctamente
|
||||
|
||||
- **Deploy** - Usado script oficial `deploy.sh` con rsync
|
||||
- Evita problemas de caché y archivos en ubicaciones incorrectas
|
||||
- Sincronización completa de directorios
|
||||
|
||||
|
||||
## [1.32.1] - 2025-12-14
|
||||
|
||||
### Fixed
|
||||
|
||||
@ -887,22 +887,29 @@ public function liabilities(Request $request)
|
||||
|
||||
/**
|
||||
* Transacciones futuras programadas
|
||||
* Incluye: transacciones pending/scheduled, cuotas de pasivos, y proyecciones de recurrencias
|
||||
*/
|
||||
public function futureTransactions(Request $request)
|
||||
{
|
||||
$this->init();
|
||||
$days = $request->get('days', 30);
|
||||
$days = (int) $request->get('days', 30);
|
||||
|
||||
$endDate = now()->addDays($days)->format('Y-m-d');
|
||||
$today = now()->format('Y-m-d');
|
||||
|
||||
$data = DB::select("
|
||||
$result = [];
|
||||
$totalIncomeConverted = 0;
|
||||
$totalExpenseConverted = 0;
|
||||
|
||||
// 1. Transacciones pendientes/scheduled (usando planned_date)
|
||||
$pendingTransactions = DB::select("
|
||||
SELECT
|
||||
t.id,
|
||||
t.description,
|
||||
t.amount,
|
||||
COALESCE(t.planned_amount, t.amount) as amount,
|
||||
t.type,
|
||||
t.effective_date,
|
||||
t.planned_date as date,
|
||||
t.status,
|
||||
COALESCE(a.currency, 'EUR') as currency,
|
||||
a.name as account_name,
|
||||
c.name as category_name,
|
||||
@ -913,16 +920,14 @@ public function futureTransactions(Request $request)
|
||||
LEFT JOIN categories c ON t.category_id = c.id
|
||||
LEFT JOIN cost_centers cc ON t.cost_center_id = cc.id
|
||||
WHERE t.user_id = ?
|
||||
AND t.effective_date > ?
|
||||
AND t.effective_date <= ?
|
||||
AND t.status IN ('pending', 'scheduled')
|
||||
AND t.planned_date >= ?
|
||||
AND t.planned_date <= ?
|
||||
AND t.deleted_at IS NULL
|
||||
ORDER BY t.effective_date ASC
|
||||
ORDER BY t.planned_date ASC
|
||||
", [$this->userId, $today, $endDate]);
|
||||
|
||||
$totalIncomeConverted = 0;
|
||||
$totalExpenseConverted = 0;
|
||||
|
||||
$result = array_map(function($row) use (&$totalIncomeConverted, &$totalExpenseConverted) {
|
||||
foreach ($pendingTransactions as $row) {
|
||||
$amount = abs($row->amount);
|
||||
$converted = $this->convertToPrimaryCurrency($amount, $row->currency);
|
||||
|
||||
@ -932,27 +937,137 @@ public function futureTransactions(Request $request)
|
||||
$totalExpenseConverted += $converted;
|
||||
}
|
||||
|
||||
return [
|
||||
$result[] = [
|
||||
'id' => $row->id,
|
||||
'description' => $row->description,
|
||||
'amount' => round($amount, 2),
|
||||
'amount_converted' => round($converted, 2),
|
||||
'currency' => $row->currency,
|
||||
'type' => $row->type,
|
||||
'date' => $row->effective_date,
|
||||
'days_until' => Carbon::parse($row->effective_date)->diffInDays(now()),
|
||||
'source_type' => 'transaction',
|
||||
'status' => $row->status,
|
||||
'date' => $row->date,
|
||||
'days_until' => max(0, Carbon::parse($row->date)->diffInDays(now(), false) * -1),
|
||||
'account' => $row->account_name,
|
||||
'category' => $row->category_name,
|
||||
'category_icon' => $row->category_icon,
|
||||
'cost_center' => $row->cost_center_name,
|
||||
];
|
||||
}, $data);
|
||||
}
|
||||
|
||||
// 2. Cuotas de pasivos pendientes
|
||||
$pendingInstallments = DB::select("
|
||||
SELECT
|
||||
li.id,
|
||||
la.name as description,
|
||||
li.installment_amount as amount,
|
||||
'debit' as type,
|
||||
li.due_date as date,
|
||||
li.status,
|
||||
la.currency,
|
||||
a.name as account_name
|
||||
FROM liability_installments li
|
||||
JOIN liability_accounts la ON li.liability_account_id = la.id
|
||||
LEFT JOIN accounts a ON la.account_id = a.id
|
||||
WHERE la.user_id = ?
|
||||
AND li.status = 'pending'
|
||||
AND li.due_date >= ?
|
||||
AND li.due_date <= ?
|
||||
AND li.deleted_at IS NULL
|
||||
ORDER BY li.due_date ASC
|
||||
", [$this->userId, $today, $endDate]);
|
||||
|
||||
foreach ($pendingInstallments as $row) {
|
||||
$amount = abs($row->amount);
|
||||
$converted = $this->convertToPrimaryCurrency($amount, $row->currency);
|
||||
$totalExpenseConverted += $converted;
|
||||
|
||||
$result[] = [
|
||||
'id' => $row->id,
|
||||
'description' => $row->description . ' (Cuota)',
|
||||
'amount' => round($amount, 2),
|
||||
'amount_converted' => round($converted, 2),
|
||||
'currency' => $row->currency,
|
||||
'type' => 'debit',
|
||||
'source_type' => 'liability_installment',
|
||||
'status' => $row->status,
|
||||
'date' => $row->date,
|
||||
'days_until' => max(0, Carbon::parse($row->date)->diffInDays(now(), false) * -1),
|
||||
'account' => $row->account_name,
|
||||
'category' => null,
|
||||
'category_icon' => null,
|
||||
'cost_center' => null,
|
||||
];
|
||||
}
|
||||
|
||||
// 3. Proyecciones de recurrencias activas
|
||||
$recurrences = DB::select("
|
||||
SELECT
|
||||
rt.id,
|
||||
rt.name,
|
||||
rt.transaction_description as description,
|
||||
rt.planned_amount as amount,
|
||||
rt.type,
|
||||
rt.frequency,
|
||||
rt.day_of_month,
|
||||
rt.start_date,
|
||||
rt.end_date,
|
||||
COALESCE(a.currency, 'EUR') as currency,
|
||||
a.name as account_name,
|
||||
c.name as category_name,
|
||||
c.icon as category_icon,
|
||||
cc.name as cost_center_name
|
||||
FROM recurring_templates rt
|
||||
LEFT JOIN accounts a ON rt.account_id = a.id
|
||||
LEFT JOIN categories c ON rt.category_id = c.id
|
||||
LEFT JOIN cost_centers cc ON rt.cost_center_id = cc.id
|
||||
WHERE rt.user_id = ?
|
||||
AND rt.is_active = 1
|
||||
AND rt.deleted_at IS NULL
|
||||
AND (rt.end_date IS NULL OR rt.end_date >= ?)
|
||||
", [$this->userId, $today]);
|
||||
|
||||
foreach ($recurrences as $rec) {
|
||||
// Calcular próximas ejecuciones dentro del período
|
||||
$nextDates = $this->getNextRecurrenceDates($rec, $today, $endDate);
|
||||
|
||||
foreach ($nextDates as $nextDate) {
|
||||
$amount = abs($rec->amount);
|
||||
$converted = $this->convertToPrimaryCurrency($amount, $rec->currency);
|
||||
|
||||
if ($rec->type === 'credit') {
|
||||
$totalIncomeConverted += $converted;
|
||||
} else {
|
||||
$totalExpenseConverted += $converted;
|
||||
}
|
||||
|
||||
$result[] = [
|
||||
'id' => $rec->id,
|
||||
'description' => $rec->name . ' (Recurrente)',
|
||||
'amount' => round($amount, 2),
|
||||
'amount_converted' => round($converted, 2),
|
||||
'currency' => $rec->currency,
|
||||
'type' => $rec->type,
|
||||
'source_type' => 'recurring',
|
||||
'status' => 'projected',
|
||||
'date' => $nextDate,
|
||||
'days_until' => max(0, Carbon::parse($nextDate)->diffInDays(now(), false) * -1),
|
||||
'account' => $rec->account_name,
|
||||
'category' => $rec->category_name,
|
||||
'category_icon' => $rec->category_icon,
|
||||
'cost_center' => $rec->cost_center_name,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Ordenar por fecha
|
||||
usort($result, fn($a, $b) => strcmp($a['date'], $b['date']));
|
||||
|
||||
return response()->json([
|
||||
'data' => $result,
|
||||
'currency' => $this->primaryCurrency,
|
||||
'summary' => [
|
||||
'total_transactions' => count($result),
|
||||
'total_items' => count($result),
|
||||
'total_income' => round($totalIncomeConverted, 2),
|
||||
'total_expense' => round($totalExpenseConverted, 2),
|
||||
'net_impact' => round($totalIncomeConverted - $totalExpenseConverted, 2),
|
||||
@ -962,24 +1077,29 @@ public function futureTransactions(Request $request)
|
||||
|
||||
/**
|
||||
* Transacciones vencidas (pendientes de pago)
|
||||
* Incluye: cuotas de pasivos vencidas y transacciones pendientes/scheduled pasadas
|
||||
*/
|
||||
public function overdueTransactions(Request $request)
|
||||
{
|
||||
$this->init();
|
||||
|
||||
$today = now()->format('Y-m-d');
|
||||
$result = [];
|
||||
$totalOverdueConverted = 0;
|
||||
|
||||
// Cuotas de pasivos vencidas
|
||||
// 1. Cuotas de pasivos vencidas
|
||||
$overdueInstallments = DB::select("
|
||||
SELECT
|
||||
li.id,
|
||||
la.name as liability_name,
|
||||
la.name as description,
|
||||
li.installment_amount as amount,
|
||||
li.due_date,
|
||||
la.currency,
|
||||
DATEDIFF(?, li.due_date) as days_overdue
|
||||
DATEDIFF(?, li.due_date) as days_overdue,
|
||||
a.name as account_name
|
||||
FROM liability_installments li
|
||||
JOIN liability_accounts la ON li.liability_account_id = la.id
|
||||
LEFT JOIN accounts a ON la.account_id = a.id
|
||||
WHERE la.user_id = ?
|
||||
AND li.status != 'paid'
|
||||
AND li.due_date < ?
|
||||
@ -987,28 +1107,76 @@ public function overdueTransactions(Request $request)
|
||||
ORDER BY li.due_date ASC
|
||||
", [$today, $this->userId, $today]);
|
||||
|
||||
$totalOverdueConverted = 0;
|
||||
$installmentsResult = array_map(function($row) use (&$totalOverdueConverted) {
|
||||
foreach ($overdueInstallments as $row) {
|
||||
$converted = $this->convertToPrimaryCurrency($row->amount, $row->currency);
|
||||
$totalOverdueConverted += $converted;
|
||||
|
||||
return [
|
||||
$result[] = [
|
||||
'id' => $row->id,
|
||||
'description' => $row->liability_name,
|
||||
'description' => $row->description . ' (Cuota)',
|
||||
'amount' => round($row->amount, 2),
|
||||
'amount_converted' => round($converted, 2),
|
||||
'currency' => $row->currency,
|
||||
'due_date' => $row->due_date,
|
||||
'days_overdue' => $row->days_overdue,
|
||||
'type' => 'liability_installment',
|
||||
'days_overdue' => (int) $row->days_overdue,
|
||||
'source_type' => 'liability_installment',
|
||||
'account' => $row->account_name,
|
||||
'category' => null,
|
||||
];
|
||||
}, $overdueInstallments);
|
||||
}
|
||||
|
||||
// 2. Transacciones pendientes/scheduled con fecha pasada
|
||||
$overdueTransactions = DB::select("
|
||||
SELECT
|
||||
t.id,
|
||||
t.description,
|
||||
COALESCE(t.planned_amount, t.amount) as amount,
|
||||
t.planned_date as due_date,
|
||||
t.type,
|
||||
t.status,
|
||||
COALESCE(a.currency, 'EUR') as currency,
|
||||
DATEDIFF(?, t.planned_date) as days_overdue,
|
||||
a.name as account_name,
|
||||
c.name as category_name
|
||||
FROM transactions t
|
||||
LEFT JOIN accounts a ON t.account_id = a.id
|
||||
LEFT JOIN categories c ON t.category_id = c.id
|
||||
WHERE t.user_id = ?
|
||||
AND t.status IN ('pending', 'scheduled')
|
||||
AND t.planned_date < ?
|
||||
AND t.deleted_at IS NULL
|
||||
ORDER BY t.planned_date ASC
|
||||
", [$today, $this->userId, $today]);
|
||||
|
||||
foreach ($overdueTransactions as $row) {
|
||||
$amount = abs($row->amount);
|
||||
$converted = $this->convertToPrimaryCurrency($amount, $row->currency);
|
||||
$totalOverdueConverted += $converted;
|
||||
|
||||
$result[] = [
|
||||
'id' => $row->id,
|
||||
'description' => $row->description,
|
||||
'amount' => round($amount, 2),
|
||||
'amount_converted' => round($converted, 2),
|
||||
'currency' => $row->currency,
|
||||
'due_date' => $row->due_date,
|
||||
'days_overdue' => (int) $row->days_overdue,
|
||||
'source_type' => 'transaction',
|
||||
'type' => $row->type,
|
||||
'status' => $row->status,
|
||||
'account' => $row->account_name,
|
||||
'category' => $row->category_name,
|
||||
];
|
||||
}
|
||||
|
||||
// Ordenar por días de atraso (más atrasado primero)
|
||||
usort($result, fn($a, $b) => $b['days_overdue'] <=> $a['days_overdue']);
|
||||
|
||||
return response()->json([
|
||||
'data' => $installmentsResult,
|
||||
'data' => $result,
|
||||
'currency' => $this->primaryCurrency,
|
||||
'summary' => [
|
||||
'total_overdue' => count($installmentsResult),
|
||||
'total_overdue' => count($result),
|
||||
'total_amount' => round($totalOverdueConverted, 2),
|
||||
],
|
||||
]);
|
||||
@ -1124,4 +1292,100 @@ public function executiveSummary(Request $request)
|
||||
}, $top5Categories),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcula las próximas fechas de ejecución de una recurrencia dentro de un período
|
||||
*/
|
||||
private function getNextRecurrenceDates($recurrence, $startDate, $endDate)
|
||||
{
|
||||
$dates = [];
|
||||
$start = Carbon::parse($startDate);
|
||||
$end = Carbon::parse($endDate);
|
||||
$recStart = Carbon::parse($recurrence->start_date);
|
||||
$recEnd = $recurrence->end_date ? Carbon::parse($recurrence->end_date) : null;
|
||||
|
||||
// Si la recurrencia termina antes del período, no hay fechas
|
||||
if ($recEnd && $recEnd->lt($start)) {
|
||||
return $dates;
|
||||
}
|
||||
|
||||
// Calcular la primera fecha dentro del período
|
||||
$current = $recStart->copy();
|
||||
|
||||
// Avanzar hasta estar dentro del período
|
||||
while ($current->lt($start)) {
|
||||
$current = $this->advanceToNextOccurrence($current, $recurrence);
|
||||
if ($current->gt($end)) {
|
||||
return $dates;
|
||||
}
|
||||
}
|
||||
|
||||
// Generar fechas hasta el fin del período
|
||||
$maxIterations = 100; // Prevenir bucles infinitos
|
||||
$iterations = 0;
|
||||
|
||||
while ($current->lte($end) && $iterations < $maxIterations) {
|
||||
// Verificar que no pase de la fecha de fin de la recurrencia
|
||||
if ($recEnd && $current->gt($recEnd)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$dates[] = $current->format('Y-m-d');
|
||||
$current = $this->advanceToNextOccurrence($current, $recurrence);
|
||||
$iterations++;
|
||||
}
|
||||
|
||||
return $dates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Avanza a la próxima ocurrencia según la frecuencia
|
||||
*/
|
||||
private function advanceToNextOccurrence($date, $recurrence)
|
||||
{
|
||||
$next = $date->copy();
|
||||
|
||||
switch ($recurrence->frequency) {
|
||||
case 'daily':
|
||||
$next->addDays($recurrence->frequency_interval ?? 1);
|
||||
break;
|
||||
case 'weekly':
|
||||
$next->addWeeks($recurrence->frequency_interval ?? 1);
|
||||
break;
|
||||
case 'biweekly':
|
||||
$next->addWeeks(2);
|
||||
break;
|
||||
case 'monthly':
|
||||
$next->addMonths($recurrence->frequency_interval ?? 1);
|
||||
if ($recurrence->day_of_month) {
|
||||
$next->day = min($recurrence->day_of_month, $next->daysInMonth);
|
||||
}
|
||||
break;
|
||||
case 'bimonthly':
|
||||
$next->addMonths(2);
|
||||
if ($recurrence->day_of_month) {
|
||||
$next->day = min($recurrence->day_of_month, $next->daysInMonth);
|
||||
}
|
||||
break;
|
||||
case 'quarterly':
|
||||
$next->addMonths(3);
|
||||
if ($recurrence->day_of_month) {
|
||||
$next->day = min($recurrence->day_of_month, $next->daysInMonth);
|
||||
}
|
||||
break;
|
||||
case 'semiannual':
|
||||
$next->addMonths(6);
|
||||
if ($recurrence->day_of_month) {
|
||||
$next->day = min($recurrence->day_of_month, $next->daysInMonth);
|
||||
}
|
||||
break;
|
||||
case 'annual':
|
||||
$next->addYears($recurrence->frequency_interval ?? 1);
|
||||
break;
|
||||
default:
|
||||
$next->addMonths(1);
|
||||
}
|
||||
|
||||
return $next;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user