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/).
|
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
|
## [1.32.1] - 2025-12-14
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@ -887,22 +887,29 @@ public function liabilities(Request $request)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Transacciones futuras programadas
|
* Transacciones futuras programadas
|
||||||
|
* Incluye: transacciones pending/scheduled, cuotas de pasivos, y proyecciones de recurrencias
|
||||||
*/
|
*/
|
||||||
public function futureTransactions(Request $request)
|
public function futureTransactions(Request $request)
|
||||||
{
|
{
|
||||||
$this->init();
|
$this->init();
|
||||||
$days = $request->get('days', 30);
|
$days = (int) $request->get('days', 30);
|
||||||
|
|
||||||
$endDate = now()->addDays($days)->format('Y-m-d');
|
$endDate = now()->addDays($days)->format('Y-m-d');
|
||||||
$today = now()->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
|
SELECT
|
||||||
t.id,
|
t.id,
|
||||||
t.description,
|
t.description,
|
||||||
t.amount,
|
COALESCE(t.planned_amount, t.amount) as amount,
|
||||||
t.type,
|
t.type,
|
||||||
t.effective_date,
|
t.planned_date as date,
|
||||||
|
t.status,
|
||||||
COALESCE(a.currency, 'EUR') as currency,
|
COALESCE(a.currency, 'EUR') as currency,
|
||||||
a.name as account_name,
|
a.name as account_name,
|
||||||
c.name as category_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 categories c ON t.category_id = c.id
|
||||||
LEFT JOIN cost_centers cc ON t.cost_center_id = cc.id
|
LEFT JOIN cost_centers cc ON t.cost_center_id = cc.id
|
||||||
WHERE t.user_id = ?
|
WHERE t.user_id = ?
|
||||||
AND t.effective_date > ?
|
AND t.status IN ('pending', 'scheduled')
|
||||||
AND t.effective_date <= ?
|
AND t.planned_date >= ?
|
||||||
|
AND t.planned_date <= ?
|
||||||
AND t.deleted_at IS NULL
|
AND t.deleted_at IS NULL
|
||||||
ORDER BY t.effective_date ASC
|
ORDER BY t.planned_date ASC
|
||||||
", [$this->userId, $today, $endDate]);
|
", [$this->userId, $today, $endDate]);
|
||||||
|
|
||||||
$totalIncomeConverted = 0;
|
foreach ($pendingTransactions as $row) {
|
||||||
$totalExpenseConverted = 0;
|
|
||||||
|
|
||||||
$result = array_map(function($row) use (&$totalIncomeConverted, &$totalExpenseConverted) {
|
|
||||||
$amount = abs($row->amount);
|
$amount = abs($row->amount);
|
||||||
$converted = $this->convertToPrimaryCurrency($amount, $row->currency);
|
$converted = $this->convertToPrimaryCurrency($amount, $row->currency);
|
||||||
|
|
||||||
@ -932,27 +937,137 @@ public function futureTransactions(Request $request)
|
|||||||
$totalExpenseConverted += $converted;
|
$totalExpenseConverted += $converted;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
$result[] = [
|
||||||
'id' => $row->id,
|
'id' => $row->id,
|
||||||
'description' => $row->description,
|
'description' => $row->description,
|
||||||
'amount' => round($amount, 2),
|
'amount' => round($amount, 2),
|
||||||
'amount_converted' => round($converted, 2),
|
'amount_converted' => round($converted, 2),
|
||||||
'currency' => $row->currency,
|
'currency' => $row->currency,
|
||||||
'type' => $row->type,
|
'type' => $row->type,
|
||||||
'date' => $row->effective_date,
|
'source_type' => 'transaction',
|
||||||
'days_until' => Carbon::parse($row->effective_date)->diffInDays(now()),
|
'status' => $row->status,
|
||||||
|
'date' => $row->date,
|
||||||
|
'days_until' => max(0, Carbon::parse($row->date)->diffInDays(now(), false) * -1),
|
||||||
'account' => $row->account_name,
|
'account' => $row->account_name,
|
||||||
'category' => $row->category_name,
|
'category' => $row->category_name,
|
||||||
'category_icon' => $row->category_icon,
|
'category_icon' => $row->category_icon,
|
||||||
'cost_center' => $row->cost_center_name,
|
'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([
|
return response()->json([
|
||||||
'data' => $result,
|
'data' => $result,
|
||||||
'currency' => $this->primaryCurrency,
|
'currency' => $this->primaryCurrency,
|
||||||
'summary' => [
|
'summary' => [
|
||||||
'total_transactions' => count($result),
|
'total_items' => count($result),
|
||||||
'total_income' => round($totalIncomeConverted, 2),
|
'total_income' => round($totalIncomeConverted, 2),
|
||||||
'total_expense' => round($totalExpenseConverted, 2),
|
'total_expense' => round($totalExpenseConverted, 2),
|
||||||
'net_impact' => round($totalIncomeConverted - $totalExpenseConverted, 2),
|
'net_impact' => round($totalIncomeConverted - $totalExpenseConverted, 2),
|
||||||
@ -962,24 +1077,29 @@ public function futureTransactions(Request $request)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Transacciones vencidas (pendientes de pago)
|
* Transacciones vencidas (pendientes de pago)
|
||||||
|
* Incluye: cuotas de pasivos vencidas y transacciones pendientes/scheduled pasadas
|
||||||
*/
|
*/
|
||||||
public function overdueTransactions(Request $request)
|
public function overdueTransactions(Request $request)
|
||||||
{
|
{
|
||||||
$this->init();
|
$this->init();
|
||||||
|
|
||||||
$today = now()->format('Y-m-d');
|
$today = now()->format('Y-m-d');
|
||||||
|
$result = [];
|
||||||
|
$totalOverdueConverted = 0;
|
||||||
|
|
||||||
// Cuotas de pasivos vencidas
|
// 1. Cuotas de pasivos vencidas
|
||||||
$overdueInstallments = DB::select("
|
$overdueInstallments = DB::select("
|
||||||
SELECT
|
SELECT
|
||||||
li.id,
|
li.id,
|
||||||
la.name as liability_name,
|
la.name as description,
|
||||||
li.installment_amount as amount,
|
li.installment_amount as amount,
|
||||||
li.due_date,
|
li.due_date,
|
||||||
la.currency,
|
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
|
FROM liability_installments li
|
||||||
JOIN liability_accounts la ON li.liability_account_id = la.id
|
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 = ?
|
WHERE la.user_id = ?
|
||||||
AND li.status != 'paid'
|
AND li.status != 'paid'
|
||||||
AND li.due_date < ?
|
AND li.due_date < ?
|
||||||
@ -987,28 +1107,76 @@ public function overdueTransactions(Request $request)
|
|||||||
ORDER BY li.due_date ASC
|
ORDER BY li.due_date ASC
|
||||||
", [$today, $this->userId, $today]);
|
", [$today, $this->userId, $today]);
|
||||||
|
|
||||||
$totalOverdueConverted = 0;
|
foreach ($overdueInstallments as $row) {
|
||||||
$installmentsResult = array_map(function($row) use (&$totalOverdueConverted) {
|
|
||||||
$converted = $this->convertToPrimaryCurrency($row->amount, $row->currency);
|
$converted = $this->convertToPrimaryCurrency($row->amount, $row->currency);
|
||||||
$totalOverdueConverted += $converted;
|
$totalOverdueConverted += $converted;
|
||||||
|
|
||||||
return [
|
$result[] = [
|
||||||
'id' => $row->id,
|
'id' => $row->id,
|
||||||
'description' => $row->liability_name,
|
'description' => $row->description . ' (Cuota)',
|
||||||
'amount' => round($row->amount, 2),
|
'amount' => round($row->amount, 2),
|
||||||
'amount_converted' => round($converted, 2),
|
'amount_converted' => round($converted, 2),
|
||||||
'currency' => $row->currency,
|
'currency' => $row->currency,
|
||||||
'due_date' => $row->due_date,
|
'due_date' => $row->due_date,
|
||||||
'days_overdue' => $row->days_overdue,
|
'days_overdue' => (int) $row->days_overdue,
|
||||||
'type' => 'liability_installment',
|
'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([
|
return response()->json([
|
||||||
'data' => $installmentsResult,
|
'data' => $result,
|
||||||
'currency' => $this->primaryCurrency,
|
'currency' => $this->primaryCurrency,
|
||||||
'summary' => [
|
'summary' => [
|
||||||
'total_overdue' => count($installmentsResult),
|
'total_overdue' => count($result),
|
||||||
'total_amount' => round($totalOverdueConverted, 2),
|
'total_amount' => round($totalOverdueConverted, 2),
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
@ -1124,4 +1292,100 @@ public function executiveSummary(Request $request)
|
|||||||
}, $top5Categories),
|
}, $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