mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2026-08-17 16:35:20 -05:00
487 lines
20 KiB
PHP
487 lines
20 KiB
PHP
<?php
|
|
|
|
/*
|
|
* CorrectAmounts.php
|
|
* Copyright (c) 2023 james@firefly-iii.org
|
|
*
|
|
* This file is part of Firefly III (https://github.com/firefly-iii).
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace FireflyIII\Console\Commands\Correction;
|
|
|
|
use FireflyIII\Console\Commands\ShowsFriendlyMessages;
|
|
use FireflyIII\Enums\TransactionTypeEnum;
|
|
use FireflyIII\Models\AutoBudget;
|
|
use FireflyIII\Models\AvailableBudget;
|
|
use FireflyIII\Models\Bill;
|
|
use FireflyIII\Models\BudgetLimit;
|
|
use FireflyIII\Models\CurrencyExchangeRate;
|
|
use FireflyIII\Models\PiggyBank;
|
|
use FireflyIII\Models\RecurrenceTransaction;
|
|
use FireflyIII\Models\RuleTrigger;
|
|
use FireflyIII\Models\Transaction;
|
|
use FireflyIII\Models\TransactionJournal;
|
|
use FireflyIII\Models\TransactionType;
|
|
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
|
|
use FireflyIII\Services\Internal\Destroy\GenericDestroyService;
|
|
use FireflyIII\Services\Internal\Destroy\JournalDestroyService;
|
|
use FireflyIII\Support\Facades\Amount;
|
|
use FireflyIII\Support\Facades\Steam;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
use ValueError;
|
|
|
|
class CorrectsAmounts extends Command
|
|
{
|
|
use ShowsFriendlyMessages;
|
|
|
|
protected $description = 'This command makes sure positive and negative amounts are recorded correctly.';
|
|
protected $signature = 'correction:amounts';
|
|
private JournalDestroyService $service;
|
|
private GenericDestroyService $genericService;
|
|
|
|
public function handle(): int
|
|
{
|
|
$this->service = new JournalDestroyService();
|
|
$this->genericService = new GenericDestroyService();
|
|
|
|
// transfers must not have foreign currency info if both accounts have the same currency.
|
|
$this->correctTransfers();
|
|
// deposits between assets and liabilities must not have foreign currency info if both accounts have the same currency.
|
|
// $this->correctDeposits();
|
|
// auto budgets must be positive
|
|
$this->fixAutoBudgets();
|
|
// available budgets must be positive
|
|
$this->fixAvailableBudgets();
|
|
// bills must be positive (both amounts)
|
|
$this->fixBills();
|
|
// budget limits must be positive
|
|
$this->fixBudgetLimits();
|
|
// currency_exchange_rates must be positive
|
|
$this->fixExchangeRates();
|
|
// piggy_banks must be positive
|
|
$this->fixPiggyBanks();
|
|
// recurrences_transactions amount must be positive
|
|
$this->fixRecurrences();
|
|
// rule_triggers must be positive or zero (amount_less, amount_more, amount_is)
|
|
$this->fixRuleTriggers();
|
|
|
|
return 0;
|
|
}
|
|
|
|
// private function correctDeposits(): void
|
|
// {
|
|
// Log::debug('Will now correct deposits.');
|
|
//
|
|
// /** @var AccountRepositoryInterface $repository */
|
|
// $repository = app(AccountRepositoryInterface::class);
|
|
// $type = TransactionType::query()->where('type', TransactionTypeEnum::DEPOSIT->value)->first();
|
|
// $journals = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
|
|
// ->whereNotNull('transactions.foreign_amount')
|
|
// ->where('transaction_journals.transaction_type_id', $type->id)
|
|
// ->distinct()
|
|
// ->get(['transaction_journals.*'])
|
|
// ;
|
|
//
|
|
// /** @var TransactionJournal $journal */
|
|
// foreach ($journals as $journal) {
|
|
// $repository->setUser($journal->user);
|
|
// $primary = Amount::getPrimaryCurrencyByUserGroup($journal->userGroup);
|
|
//
|
|
// $valid = $this->validateJournal($journal);
|
|
// if (false === $valid) {
|
|
// // Log::debug(sprintf('Journal #%d does not need to be fixed or is invalid (see previous messages)', $journal->id));
|
|
//
|
|
// continue;
|
|
// }
|
|
// Log::debug(sprintf('Journal #%d is ready to be corrected (if necessary).', $journal->id));
|
|
// $source = $journal->transactions()->where('amount', '<', '0')->first();
|
|
// $destination = $journal->transactions()->where('amount', '>', '0')->first();
|
|
// $sourceAccount = $source->account;
|
|
// $destAccount = $destination->account;
|
|
// $sourceCurrency = $repository->getAccountCurrency($sourceAccount) ?? $primary;
|
|
// $destCurrency = $repository->getAccountCurrency($destAccount) ?? $primary;
|
|
// Log::debug(sprintf('Currency of source account #%d "%s" is %s', $sourceAccount->id, $sourceAccount->name, $sourceCurrency->code));
|
|
// Log::debug(sprintf('Currency of destination account #%d "%s" is %s', $destAccount->id, $destAccount->name, $destCurrency->code));
|
|
// if ($sourceCurrency->id === $destCurrency->id) {
|
|
// Log::debug('Both accounts have the same currency. Removing foreign currency info.');
|
|
// $source->foreign_currency_id = null;
|
|
// $source->foreign_amount = null;
|
|
// $source->save();
|
|
// $destination->foreign_currency_id = null;
|
|
// $destination->foreign_amount = null;
|
|
// // also make sure that both transactions use the same amounts and currencies, since the currency is the same anyway.
|
|
// $destination->amount = bcmul($source->amount, '-1');
|
|
// $destination->save();
|
|
//
|
|
// continue;
|
|
// }
|
|
//
|
|
// // validate source transaction
|
|
// if ($destCurrency->id !== $source->foreign_currency_id) {
|
|
// Log::debug(sprintf(
|
|
// '[a] Journal #%d: transaction #%d refers to foreign currency "%s" but should refer to "%s".',
|
|
// $journal->id,
|
|
// $source->id,
|
|
// $source->foreignCurrency->code,
|
|
// $destCurrency->code
|
|
// ));
|
|
// $source->foreign_currency_id = $destCurrency->id;
|
|
// $source->save();
|
|
// }
|
|
// if ($sourceCurrency->id !== $source->transaction_currency_id) {
|
|
// Log::debug(sprintf(
|
|
// '[b] Journal #%d: transaction #%d refers to currency "%s" but should refer to "%s".',
|
|
// $journal->id,
|
|
// $source->id,
|
|
// $source->transactionCurrency->code,
|
|
// $sourceCurrency->code
|
|
// ));
|
|
// $source->transaction_currency_id = $sourceCurrency->id;
|
|
// $source->save();
|
|
// }
|
|
//
|
|
// // validate destination:
|
|
// if ($sourceCurrency->id !== $destination->foreign_currency_id) {
|
|
// Log::debug(sprintf(
|
|
// '[c] Journal #%d: transaction #%d refers to foreign currency "%s" but should refer to "%s".',
|
|
// $journal->id,
|
|
// $destination->id,
|
|
// $destination->foreignCurrency->code,
|
|
// $sourceCurrency->code
|
|
// ));
|
|
// $destination->foreign_currency_id = $sourceCurrency->id;
|
|
// $destination->save();
|
|
// }
|
|
//
|
|
// if ($destCurrency->id !== $destination->transaction_currency_id) {
|
|
// Log::debug(sprintf(
|
|
// '[d] Journal #%d: transaction #%d refers to currency "%s" but should refer to "%s".',
|
|
// $journal->id,
|
|
// $destination->id,
|
|
// $destination->transactionCurrency->code,
|
|
// $destCurrency->code
|
|
// ));
|
|
// $destination->transaction_currency_id = $destCurrency->id;
|
|
// $destination->save();
|
|
// }
|
|
// Log::debug(sprintf('Done with journal #%d.', $journal->id));
|
|
// }
|
|
// }
|
|
|
|
private function correctTransfers(): void
|
|
{
|
|
Log::debug('Will now correct transfers.');
|
|
|
|
/** @var AccountRepositoryInterface $repository */
|
|
$repository = app(AccountRepositoryInterface::class);
|
|
$type = TransactionType::query()->where('type', TransactionTypeEnum::TRANSFER->value)->first();
|
|
$journals = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
|
|
->whereNotNull('transactions.foreign_amount')
|
|
->where('transaction_journals.transaction_type_id', $type->id)
|
|
->distinct()
|
|
->get(['transaction_journals.*'])
|
|
;
|
|
|
|
/** @var TransactionJournal $journal */
|
|
foreach ($journals as $journal) {
|
|
$repository->setUser($journal->user);
|
|
$primary = Amount::getPrimaryCurrencyByUserGroup($journal->userGroup);
|
|
|
|
$valid = $this->validateJournal($journal);
|
|
if (false === $valid) {
|
|
// Log::debug(sprintf('Journal #%d does not need to be fixed or is invalid (see previous messages)', $journal->id));
|
|
|
|
continue;
|
|
}
|
|
Log::debug(sprintf('Journal #%d is ready to be corrected (if necessary).', $journal->id));
|
|
$source = $journal->transactions()->where('amount', '<', '0')->first();
|
|
$destination = $journal->transactions()->where('amount', '>', '0')->first();
|
|
$sourceAccount = $source->account;
|
|
$destAccount = $destination->account;
|
|
$sourceCurrency = $repository->getAccountCurrency($sourceAccount) ?? $primary;
|
|
$destCurrency = $repository->getAccountCurrency($destAccount) ?? $primary;
|
|
Log::debug(sprintf('Currency of source account #%d "%s" is %s', $sourceAccount->id, $sourceAccount->name, $sourceCurrency->code));
|
|
Log::debug(sprintf('Currency of destination account #%d "%s" is %s', $destAccount->id, $destAccount->name, $destCurrency->code));
|
|
|
|
if ($sourceCurrency->id === $destCurrency->id) {
|
|
Log::debug('Both accounts have the same currency. Removing foreign currency info.');
|
|
$source->foreign_currency_id = null;
|
|
$source->foreign_amount = null;
|
|
$source->save();
|
|
$destination->foreign_currency_id = null;
|
|
$destination->foreign_amount = null;
|
|
$destination->save();
|
|
|
|
continue;
|
|
}
|
|
|
|
// validate source transaction
|
|
if ($destCurrency->id !== $source->foreign_currency_id) {
|
|
Log::debug(sprintf(
|
|
'[a] Journal #%d: transaction #%d refers to foreign currency "%s" but should refer to "%s".',
|
|
$journal->id,
|
|
$source->id,
|
|
$source->foreignCurrency->code,
|
|
$destCurrency->code
|
|
));
|
|
$source->foreign_currency_id = $destCurrency->id;
|
|
$source->save();
|
|
}
|
|
if ($sourceCurrency->id !== $source->transaction_currency_id) {
|
|
Log::debug(sprintf(
|
|
'[b] Journal #%d: transaction #%d refers to currency "%s" but should refer to "%s".',
|
|
$journal->id,
|
|
$source->id,
|
|
$source->transactionCurrency->code,
|
|
$sourceCurrency->code
|
|
));
|
|
$source->transaction_currency_id = $sourceCurrency->id;
|
|
$source->save();
|
|
}
|
|
|
|
// validate destination:
|
|
if ($sourceCurrency->id !== $destination->foreign_currency_id) {
|
|
Log::debug(sprintf(
|
|
'[c] Journal #%d: transaction #%d refers to foreign currency "%s" but should refer to "%s".',
|
|
$journal->id,
|
|
$destination->id,
|
|
$destination->foreignCurrency->code,
|
|
$sourceCurrency->code
|
|
));
|
|
$destination->foreign_currency_id = $sourceCurrency->id;
|
|
$destination->save();
|
|
}
|
|
|
|
if ($destCurrency->id !== $destination->transaction_currency_id) {
|
|
Log::debug(sprintf(
|
|
'[d] Journal #%d: transaction #%d refers to currency "%s" but should refer to "%s".',
|
|
$journal->id,
|
|
$destination->id,
|
|
$destination->transactionCurrency->code,
|
|
$destCurrency->code
|
|
));
|
|
$destination->transaction_currency_id = $destCurrency->id;
|
|
$destination->save();
|
|
}
|
|
Log::debug(sprintf('Done with journal #%d.', $journal->id));
|
|
}
|
|
}
|
|
|
|
private function deleteJournal(TransactionJournal $journal): void
|
|
{
|
|
$this->service->destroy($journal);
|
|
}
|
|
|
|
private function fixAutoBudgets(): void
|
|
{
|
|
$count = AutoBudget::query()
|
|
->where('amount', '<', 0)
|
|
->update(['amount' => DB::raw('amount * -1')])
|
|
;
|
|
if (0 === $count) {
|
|
return;
|
|
}
|
|
$this->friendlyInfo(sprintf('Corrected %d auto budget amount(s).', $count));
|
|
}
|
|
|
|
private function fixAvailableBudgets(): void
|
|
{
|
|
$count = AvailableBudget::query()
|
|
->where('amount', '<', 0)
|
|
->update(['amount' => DB::raw('amount * -1')])
|
|
;
|
|
if (0 === $count) {
|
|
return;
|
|
}
|
|
$this->friendlyInfo(sprintf('Corrected %d available budget amount(s).', $count));
|
|
}
|
|
|
|
private function fixBills(): void
|
|
{
|
|
$count = 0;
|
|
$count += Bill::query()
|
|
->where('amount_max', '<', 0)
|
|
->update(['amount_max' => DB::raw('amount_max * -1')])
|
|
;
|
|
$count += Bill::query()
|
|
->where('amount_min', '<', 0)
|
|
->update(['amount_min' => DB::raw('amount_min * -1')])
|
|
;
|
|
if (0 === $count) {
|
|
return;
|
|
}
|
|
$this->friendlyInfo(sprintf('Corrected %d bill amount(s).', $count));
|
|
}
|
|
|
|
private function fixBudgetLimits(): void
|
|
{
|
|
$count = BudgetLimit::query()
|
|
->where('amount', '<', 0)
|
|
->update(['amount' => DB::raw('amount * -1')])
|
|
;
|
|
if (0 === $count) {
|
|
return;
|
|
}
|
|
$this->friendlyInfo(sprintf('Corrected %d budget limit amount(s).', $count));
|
|
}
|
|
|
|
private function fixExchangeRates(): void
|
|
{
|
|
$count = CurrencyExchangeRate::query()
|
|
->where('rate', '<', 0)
|
|
->update(['rate' => DB::raw('rate * -1')])
|
|
;
|
|
if (0 === $count) {
|
|
return;
|
|
}
|
|
$this->friendlyInfo(sprintf('Corrected %d currency exchange rate(s).', $count));
|
|
}
|
|
|
|
private function fixPiggyBanks(): void
|
|
{
|
|
$count = PiggyBank::query()
|
|
->where('target_amount', '<', 0)
|
|
->update(['target_amount' => DB::raw('target_amount * -1')])
|
|
;
|
|
if (0 === $count) {
|
|
return;
|
|
}
|
|
$this->friendlyInfo(sprintf('Corrected %d piggy bank amount(s).', $count));
|
|
}
|
|
|
|
private function fixRecurrences(): void
|
|
{
|
|
$count = 0;
|
|
$count += RecurrenceTransaction::query()
|
|
->where('amount', '<', 0)
|
|
->update(['amount' => DB::raw('amount * -1')])
|
|
;
|
|
$count += RecurrenceTransaction::query()
|
|
->where('foreign_amount', '<', 0)
|
|
->update(['foreign_amount' => DB::raw('foreign_amount * -1')])
|
|
;
|
|
if (0 === $count) {
|
|
return;
|
|
}
|
|
$this->friendlyInfo(sprintf('Corrected %d recurring transaction amount(s).', $count));
|
|
}
|
|
|
|
private function fixRuleTrigger(RuleTrigger $item): bool
|
|
{
|
|
try {
|
|
$check = bccomp((string) $item->trigger_value, '0');
|
|
} catch (ValueError) {
|
|
$this->friendlyError(sprintf(
|
|
'Rule #%d contained invalid %s-trigger "%s". The trigger has been removed, and the rule is disabled.',
|
|
$item->rule_id,
|
|
$item->trigger_type,
|
|
$item->trigger_value
|
|
));
|
|
$item->rule->active = false;
|
|
$item->rule->save();
|
|
$this->genericService->deleteRuleTrigger($item);
|
|
|
|
return false;
|
|
}
|
|
if (-1 === $check) {
|
|
$item->trigger_value = Steam::positive($item->trigger_value);
|
|
$item->save();
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Foreach loop is unavoidable here.
|
|
*/
|
|
private function fixRuleTriggers(): void
|
|
{
|
|
$set = RuleTrigger::query()->whereIn('trigger_type', ['amount_less', 'amount_more', 'amount_is'])->get();
|
|
$fixed = 0;
|
|
|
|
/** @var RuleTrigger $item */
|
|
foreach ($set as $item) {
|
|
$result = $this->fixRuleTrigger($item);
|
|
if ($result) {
|
|
++$fixed;
|
|
}
|
|
}
|
|
if (0 === $fixed) {
|
|
return;
|
|
}
|
|
$this->friendlyInfo(sprintf('Corrected %d rule trigger amount(s).', $fixed));
|
|
}
|
|
|
|
private function validateJournal(TransactionJournal $journal): bool
|
|
{
|
|
$countSource = $journal->transactions()->where('amount', '<', 0)->count();
|
|
$countDest = $journal->transactions()->where('amount', '>', 0)->count();
|
|
|
|
if (1 !== $countSource || 1 !== $countDest) {
|
|
$this->friendlyError(sprintf('Transaction journal #%d has bad transaction information. Will delete.', $journal->id));
|
|
$this->deleteJournal($journal);
|
|
Log::error(sprintf('Transaction journal #%d has bad transaction information. Will delete.', $journal->id));
|
|
|
|
return false;
|
|
}
|
|
|
|
/** @var null|Transaction $source */
|
|
$source = $journal->transactions()->where('amount', '<', 0)->first();
|
|
|
|
/** @var null|Transaction $destination */
|
|
$destination = $journal->transactions()->where('amount', '>', 0)->first();
|
|
|
|
if (null === $source || null === $destination) {
|
|
$this->friendlyError(sprintf('Could not find source OR destination for journal #%d .', $journal->id));
|
|
Log::error(sprintf('Could not find source OR destination for journal #%d .', $journal->id));
|
|
$this->deleteJournal($journal);
|
|
|
|
return false;
|
|
}
|
|
if (null === $source->foreign_currency_id || null === $destination->foreign_currency_id) {
|
|
// Log::debug('No foreign currency information is present, can safely continue with other transactions.');
|
|
|
|
return false;
|
|
}
|
|
if (null === $source->foreign_amount || null === $destination->foreign_amount) {
|
|
$this->friendlyError(sprintf('Transactions of journal #%d have no foreign amount, but have foreign currency info. Will reset this.', $journal->id));
|
|
$source->foreign_currency_id = null;
|
|
$source->save();
|
|
$destination->foreign_currency_id = null;
|
|
$source->save();
|
|
|
|
return false;
|
|
}
|
|
|
|
$sourceAccount = $source->account;
|
|
$destAccount = $destination->account;
|
|
if (null === $sourceAccount || null === $destAccount) {
|
|
$this->friendlyError(sprintf('Could not find accounts for journal #%d,', $journal->id));
|
|
$this->deleteJournal($journal);
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|