v1.43.25 - REESCRITA COMPLETA: Lógica de projeção corrigida
This commit is contained in:
parent
83a4d91029
commit
d47201bca7
14
CHANGELOG.md
14
CHANGELOG.md
@ -5,6 +5,20 @@ 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.43.25] - 2025-12-16
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- 🐛 **REESCRITA COMPLETA - Projeção de Saldo Widget**
|
||||||
|
- **Bug crítico corrigido**: Estava duplicando valores (templates + instâncias)
|
||||||
|
- **Bug crítico corrigido**: Lógica de overdue invertida (somava ao invés de subtrair)
|
||||||
|
- Nova lógica simplificada e correta:
|
||||||
|
* Ponto inicial = Saldo atual real (current_balance das contas)
|
||||||
|
* Vencidos aplicados imediatamente no primeiro ponto (bizum 94€ + aluguel 1003€ = -1097€)
|
||||||
|
* Apenas recurring_instances (não templates) são processadas
|
||||||
|
* Vencidos processados uma única vez no início
|
||||||
|
* Futuros processados em cada período (dia/semana)
|
||||||
|
- Cálculo correto: 64.55€ (saldo) - 1097.81€ (vencidos) = -1033.26€ (projeção inicial)
|
||||||
|
|
||||||
## [1.43.24] - 2025-12-16
|
## [1.43.24] - 2025-12-16
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@ -1100,7 +1100,7 @@ public function projectionChart(Request $request)
|
|||||||
$today = Carbon::today();
|
$today = Carbon::today();
|
||||||
$endDate = $today->copy()->addMonths($months);
|
$endDate = $today->copy()->addMonths($months);
|
||||||
|
|
||||||
// Obter saldo atual total das contas (simplificado - assumindo mesma moeda)
|
// Obter saldo atual total das contas (valor REAL de hoje)
|
||||||
$currentBalance = DB::selectOne("
|
$currentBalance = DB::selectOne("
|
||||||
SELECT COALESCE(SUM(current_balance), 0) as total
|
SELECT COALESCE(SUM(current_balance), 0) as total
|
||||||
FROM accounts
|
FROM accounts
|
||||||
@ -1115,60 +1115,9 @@ public function projectionChart(Request $request)
|
|||||||
$interval = $months <= 3 ? 'day' : 'week';
|
$interval = $months <= 3 ? 'day' : 'week';
|
||||||
$current = $today->copy();
|
$current = $today->copy();
|
||||||
|
|
||||||
// Buscar recorrências ativas
|
// =========================================================================
|
||||||
$recurrences = DB::select("
|
// BUSCAR INSTÂNCIAS DE RECORRÊNCIAS PENDENTES (incluindo VENCIDAS)
|
||||||
SELECT
|
// =========================================================================
|
||||||
rt.id,
|
|
||||||
rt.name,
|
|
||||||
rt.planned_amount,
|
|
||||||
rt.type,
|
|
||||||
rt.frequency,
|
|
||||||
rt.day_of_month,
|
|
||||||
rt.start_date,
|
|
||||||
rt.end_date,
|
|
||||||
COALESCE(a.currency, 'EUR') as currency
|
|
||||||
FROM recurring_templates rt
|
|
||||||
LEFT JOIN accounts a ON rt.account_id = a.id
|
|
||||||
WHERE rt.user_id = ?
|
|
||||||
AND rt.is_active = 1
|
|
||||||
AND rt.deleted_at IS NULL
|
|
||||||
", [$this->userId]);
|
|
||||||
|
|
||||||
// Buscar parcelas de passivos pendentes
|
|
||||||
$liabilityInstallments = DB::select("
|
|
||||||
SELECT
|
|
||||||
li.due_date,
|
|
||||||
li.installment_amount as amount,
|
|
||||||
la.currency
|
|
||||||
FROM liability_installments li
|
|
||||||
JOIN liability_accounts la ON li.liability_account_id = la.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
|
|
||||||
", [$this->userId, $today->toDateString(), $endDate->toDateString()]);
|
|
||||||
|
|
||||||
// Buscar transações agendadas/pendentes (incluindo atrasadas)
|
|
||||||
$scheduledTransactions = DB::select("
|
|
||||||
SELECT
|
|
||||||
COALESCE(t.planned_date, t.effective_date) as date,
|
|
||||||
t.amount,
|
|
||||||
t.type,
|
|
||||||
COALESCE(a.currency, 'EUR') as currency,
|
|
||||||
CASE WHEN COALESCE(t.planned_date, t.effective_date) < ? THEN 1 ELSE 0 END as is_overdue
|
|
||||||
FROM transactions t
|
|
||||||
LEFT JOIN accounts a ON t.account_id = a.id
|
|
||||||
WHERE t.user_id = ?
|
|
||||||
AND t.status IN ('pending', 'scheduled')
|
|
||||||
AND COALESCE(t.planned_date, t.effective_date) <= ?
|
|
||||||
AND t.deleted_at IS NULL
|
|
||||||
AND {$this->excludeTransfers()}
|
|
||||||
ORDER BY COALESCE(t.planned_date, t.effective_date)
|
|
||||||
", [$today->toDateString(), $this->userId, $endDate->toDateString()]);
|
|
||||||
|
|
||||||
// Buscar instâncias de recorrências pendentes (incluindo atrasadas)
|
|
||||||
$recurringInstances = DB::select("
|
$recurringInstances = DB::select("
|
||||||
SELECT
|
SELECT
|
||||||
ri.due_date as date,
|
ri.due_date as date,
|
||||||
@ -1186,46 +1135,94 @@ public function projectionChart(Request $request)
|
|||||||
ORDER BY ri.due_date
|
ORDER BY ri.due_date
|
||||||
", [$today->toDateString(), $this->userId, $endDate->toDateString()]);
|
", [$today->toDateString(), $this->userId, $endDate->toDateString()]);
|
||||||
|
|
||||||
// Separar transações atrasadas para processar primeiro
|
// =========================================================================
|
||||||
$overdueTransactions = array_filter($scheduledTransactions, fn($tx) => $tx->is_overdue);
|
// BUSCAR TRANSAÇÕES PENDENTES/AGENDADAS (incluindo VENCIDAS)
|
||||||
$futureTransactions = array_filter($scheduledTransactions, fn($tx) => !$tx->is_overdue);
|
// =========================================================================
|
||||||
|
$scheduledTransactions = DB::select("
|
||||||
|
SELECT
|
||||||
|
COALESCE(t.planned_date, t.effective_date) as date,
|
||||||
|
t.amount,
|
||||||
|
t.type,
|
||||||
|
COALESCE(a.currency, 'EUR') as currency,
|
||||||
|
CASE WHEN COALESCE(t.planned_date, t.effective_date) < ? THEN 1 ELSE 0 END as is_overdue
|
||||||
|
FROM transactions t
|
||||||
|
LEFT JOIN accounts a ON t.account_id = a.id
|
||||||
|
WHERE t.user_id = ?
|
||||||
|
AND t.status IN ('pending', 'scheduled')
|
||||||
|
AND COALESCE(t.planned_date, t.effective_date) <= ?
|
||||||
|
AND t.deleted_at IS NULL
|
||||||
|
AND {$this->excludeTransfers()}
|
||||||
|
ORDER BY COALESCE(t.planned_date, t.effective_date)
|
||||||
|
", [$today->toDateString(), $this->userId, $endDate->toDateString()]);
|
||||||
|
|
||||||
// Separar instâncias recorrentes atrasadas
|
// =========================================================================
|
||||||
$overdueRecurringInstances = array_filter($recurringInstances, fn($ri) => $ri->is_overdue);
|
// BUSCAR PARCELAS DE PASSIVOS PENDENTES (incluindo VENCIDAS)
|
||||||
$futureRecurringInstances = array_filter($recurringInstances, fn($ri) => !$ri->is_overdue);
|
// =========================================================================
|
||||||
|
$liabilityInstallments = DB::select("
|
||||||
|
SELECT
|
||||||
|
li.due_date as date,
|
||||||
|
li.installment_amount as amount,
|
||||||
|
la.currency,
|
||||||
|
CASE WHEN li.due_date < ? THEN 1 ELSE 0 END as is_overdue
|
||||||
|
FROM liability_installments li
|
||||||
|
JOIN liability_accounts la ON li.liability_account_id = la.id
|
||||||
|
WHERE la.user_id = ?
|
||||||
|
AND li.status = 'pending'
|
||||||
|
AND li.due_date <= ?
|
||||||
|
AND li.deleted_at IS NULL
|
||||||
|
ORDER BY li.due_date
|
||||||
|
", [$today->toDateString(), $this->userId, $endDate->toDateString()]);
|
||||||
|
|
||||||
// Processar transações atrasadas ANTES do ponto inicial
|
// =========================================================================
|
||||||
foreach ($overdueTransactions as $tx) {
|
// PROCESSAR VENCIDOS: aplicar no saldo inicial
|
||||||
$amount = $this->convertToPrimaryCurrency(abs($tx->amount), $tx->currency);
|
// =========================================================================
|
||||||
if ($tx->type === 'credit') {
|
$overdueImpact = 0;
|
||||||
$currentBalance += $amount;
|
|
||||||
} else {
|
foreach ($recurringInstances as $ri) {
|
||||||
$currentBalance -= $amount;
|
if ($ri->is_overdue) {
|
||||||
|
$amount = $this->convertToPrimaryCurrency(abs($ri->amount), $ri->currency);
|
||||||
|
if ($ri->type === 'credit') {
|
||||||
|
$overdueImpact += $amount;
|
||||||
|
} else {
|
||||||
|
$overdueImpact -= $amount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$runningBalance = $currentBalance;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Processar instâncias recorrentes atrasadas ANTES do ponto inicial
|
foreach ($scheduledTransactions as $tx) {
|
||||||
foreach ($overdueRecurringInstances as $ri) {
|
if ($tx->is_overdue) {
|
||||||
$amount = $this->convertToPrimaryCurrency(abs($ri->amount), $ri->currency);
|
$amount = $this->convertToPrimaryCurrency(abs($tx->amount), $tx->currency);
|
||||||
if ($ri->type === 'credit') {
|
if ($tx->type === 'credit') {
|
||||||
$currentBalance += $amount;
|
$overdueImpact += $amount;
|
||||||
} else {
|
} else {
|
||||||
$currentBalance -= $amount;
|
$overdueImpact -= $amount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$runningBalance = $currentBalance;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ponto inicial (já inclui o impacto das transações atrasadas)
|
foreach ($liabilityInstallments as $inst) {
|
||||||
|
if ($inst->is_overdue) {
|
||||||
|
$amount = $this->convertToPrimaryCurrency(abs($inst->amount), $inst->currency);
|
||||||
|
$overdueImpact -= $amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aplicar impacto dos vencidos ao saldo inicial
|
||||||
|
$runningBalance += $overdueImpact;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// PONTO INICIAL = SALDO ATUAL (sem modificações)
|
||||||
|
// =========================================================================
|
||||||
$dataPoints[] = [
|
$dataPoints[] = [
|
||||||
'date' => $today->toDateString(),
|
'date' => $today->toDateString(),
|
||||||
'balance' => round($runningBalance, 2),
|
'balance' => round($runningBalance, 2),
|
||||||
'label' => $today->format('d/m'),
|
'label' => $today->format('d/m'),
|
||||||
'isToday' => true,
|
'isToday' => true,
|
||||||
'has_overdue' => count($overdueTransactions) + count($overdueRecurringInstances) > 0,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Gerar pontos até a data final
|
// =========================================================================
|
||||||
|
// GERAR PROJEÇÃO FUTURA
|
||||||
|
// =========================================================================
|
||||||
while ($current->lt($endDate)) {
|
while ($current->lt($endDate)) {
|
||||||
if ($interval === 'day') {
|
if ($interval === 'day') {
|
||||||
$current->addDay();
|
$current->addDay();
|
||||||
@ -1238,12 +1235,11 @@ public function projectionChart(Request $request)
|
|||||||
$periodStart = $dataPoints[count($dataPoints) - 1]['date'];
|
$periodStart = $dataPoints[count($dataPoints) - 1]['date'];
|
||||||
$periodEnd = $current->toDateString();
|
$periodEnd = $current->toDateString();
|
||||||
|
|
||||||
// Somar recorrências neste período
|
// Processar instâncias de recorrências neste período (SOMENTE futuras, não vencidas)
|
||||||
foreach ($recurrences as $rec) {
|
foreach ($recurringInstances as $ri) {
|
||||||
$dates = $this->getNextRecurrenceDates($rec, $periodStart, $periodEnd);
|
if (!$ri->is_overdue && $ri->date > $periodStart && $ri->date <= $periodEnd) {
|
||||||
foreach ($dates as $date) {
|
$amount = $this->convertToPrimaryCurrency(abs($ri->amount), $ri->currency);
|
||||||
$amount = $this->convertToPrimaryCurrency(abs($rec->planned_amount), $rec->currency);
|
if ($ri->type === 'credit') {
|
||||||
if ($rec->type === 'credit') {
|
|
||||||
$runningBalance += $amount;
|
$runningBalance += $amount;
|
||||||
} else {
|
} else {
|
||||||
$runningBalance -= $amount;
|
$runningBalance -= $amount;
|
||||||
@ -1251,17 +1247,9 @@ public function projectionChart(Request $request)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Somar parcelas de passivos neste período
|
// Processar transações agendadas neste período (SOMENTE futuras, não vencidas)
|
||||||
foreach ($liabilityInstallments as $inst) {
|
foreach ($scheduledTransactions as $tx) {
|
||||||
if ($inst->due_date > $periodStart && $inst->due_date <= $periodEnd) {
|
if (!$tx->is_overdue && $tx->date > $periodStart && $tx->date <= $periodEnd) {
|
||||||
$amount = $this->convertToPrimaryCurrency(abs($inst->amount), $inst->currency);
|
|
||||||
$runningBalance -= $amount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Somar transações agendadas neste período (apenas futuras)
|
|
||||||
foreach ($futureTransactions as $tx) {
|
|
||||||
if ($tx->date > $periodStart && $tx->date <= $periodEnd) {
|
|
||||||
$amount = $this->convertToPrimaryCurrency(abs($tx->amount), $tx->currency);
|
$amount = $this->convertToPrimaryCurrency(abs($tx->amount), $tx->currency);
|
||||||
if ($tx->type === 'credit') {
|
if ($tx->type === 'credit') {
|
||||||
$runningBalance += $amount;
|
$runningBalance += $amount;
|
||||||
@ -1271,15 +1259,11 @@ public function projectionChart(Request $request)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Somar instâncias recorrentes neste período (apenas futuras)
|
// Processar parcelas de passivos neste período (SOMENTE futuras, não vencidas)
|
||||||
foreach ($futureRecurringInstances as $ri) {
|
foreach ($liabilityInstallments as $inst) {
|
||||||
if ($ri->date > $periodStart && $ri->date <= $periodEnd) {
|
if (!$inst->is_overdue && $inst->date > $periodStart && $inst->date <= $periodEnd) {
|
||||||
$amount = $this->convertToPrimaryCurrency(abs($ri->amount), $ri->currency);
|
$amount = $this->convertToPrimaryCurrency(abs($inst->amount), $inst->currency);
|
||||||
if ($ri->type === 'credit') {
|
$runningBalance -= $amount;
|
||||||
$runningBalance += $amount;
|
|
||||||
} else {
|
|
||||||
$runningBalance -= $amount;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user