diff --git a/app/Console/Commands/Upgrade/JournalCurrencies.php b/app/Console/Commands/Upgrade/JournalCurrencies.php index 0c12eeef09..1333eaf820 100644 --- a/app/Console/Commands/Upgrade/JournalCurrencies.php +++ b/app/Console/Commands/Upgrade/JournalCurrencies.php @@ -332,41 +332,9 @@ class JournalCurrencies extends Command return; } - // has no currency ID? Must have, so fill in using account preference: - if (null === $source->transaction_currency_id) { - $source->transaction_currency_id = (int)$sourceCurrency->id; - $message = sprintf('Transaction #%d has no currency setting, now set to %s.', $source->id, $sourceCurrency->code); - Log::debug($message); - $this->line($message); - $this->count++; - $source->save(); - } - - // does not match the source account (see above)? Can be fixed - // when mismatch in transaction and NO foreign amount is set: - if (!((int)$source->transaction_currency_id === (int)$sourceCurrency->id) && null === $source->foreign_amount) { - $message = sprintf( - 'Transaction #%d has a currency setting #%d that should be #%d. Amount remains %s, currency is changed.', - $source->id, - $source->transaction_currency_id, - $sourceCurrency->id, - $source->amount - ); - Log::debug($message); - $this->line($message); - $this->count++; - $source->transaction_currency_id = (int)$sourceCurrency->id; - $source->save(); - } - - - if (null === $destCurrency) { - $message = sprintf('Account #%d ("%s") must have currency preference but has none.', $destAccount->id, $destAccount->name); - Log::error($message); - $this->line($message); - - return; - } + $this->noSourceAccountCurrency($source, $sourceCurrency); + $this->unmatchedSourceTransaction($source, $sourceCurrency); + $this->noDestAccountCurrency($destAccount, $destCurrency); // if the destination account currency is the same, both foreign_amount and foreign_currency_id must be NULL for both transactions: if ((int)$destCurrency->id === (int)$sourceCurrency->id) { @@ -490,4 +458,62 @@ class JournalCurrencies extends Command $this->updateTransactionCurrency($transfer, $sourceTransaction, $destTransaction); $this->updateJournalCurrency($transfer, $sourceTransaction); } + + /** + * Has no currency ID? Must have, so fill in using account preference. + * + * @param Transaction $source + * @param TransactionCurrency $sourceCurrency + */ + private function noSourceAccountCurrency(Transaction $source, ?TransactionCurrency $sourceCurrency): void + { + if (null === $source->transaction_currency_id && null !== $sourceCurrency) { + $source->transaction_currency_id = (int)$sourceCurrency->id; + $message = sprintf('Transaction #%d has no currency setting, now set to %s.', $source->id, $sourceCurrency->code); + Log::debug($message); + $this->line($message); + $this->count++; + $source->save(); + } + } + + /** + * Does not match the source account (see above)? Can be fixed + * when mismatch in transaction and NO foreign amount is set. + * + * @param Transaction $source + * @param TransactionCurrency $sourceCurrency|null + */ + private function unmatchedSourceTransaction(Transaction $source, ?TransactionCurrency $sourceCurrency): void + { + if (null !== $sourceCurrency && !((int)$source->transaction_currency_id === (int)$sourceCurrency->id) && null === $source->foreign_amount) { + $message = sprintf( + 'Transaction #%d has a currency setting #%d that should be #%d. Amount remains %s, currency is changed.', + $source->id, + $source->transaction_currency_id, + $sourceCurrency->id, + $source->amount + ); + Log::debug($message); + $this->line($message); + $this->count++; + $source->transaction_currency_id = (int)$sourceCurrency->id; + $source->save(); + } + } + + /** + * @param Account $destAccount + * @param TransactionCurrency|null $destCurrency + */ + private function noDestAccountCurrency(Account $destAccount, ?TransactionCurrency $destCurrency): void + { + if (null === $destCurrency) { + $message = sprintf('Account #%d ("%s") must have currency preference but has none.', $destAccount->id, $destAccount->name); + Log::error($message); + $this->line($message); + + return; + } + } } \ No newline at end of file diff --git a/app/Console/Commands/Upgrade/OtherCurrenciesCorrections.php b/app/Console/Commands/Upgrade/OtherCurrenciesCorrections.php new file mode 100644 index 0000000000..6a235c686d --- /dev/null +++ b/app/Console/Commands/Upgrade/OtherCurrenciesCorrections.php @@ -0,0 +1,294 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Console\Commands\Upgrade; + +use FireflyIII\Models\Account; +use FireflyIII\Models\AccountType; +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\TransactionType; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; +use FireflyIII\Repositories\Journal\JournalRepositoryInterface; +use Illuminate\Console\Command; + +/** + * Class OtherCurrenciesCorrections + */ +class OtherCurrenciesCorrections extends Command +{ + + public const CONFIG_NAME = '4780_other_currencies'; + /** + * The console command description. + * + * @var string + */ + protected $description = 'Update all journal currency information.'; + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'firefly-iii:other-currencies {--F|force : Force the execution of this command.}'; + /** @var array */ + private $accountCurrencies; + /** @var AccountRepositoryInterface */ + private $accountRepos; + /** @var CurrencyRepositoryInterface */ + private $currencyRepos; + /** @var JournalRepositoryInterface */ + private $journalRepos; + /** @var int */ + private $count; + + /** + * JournalCurrencies constructor. + */ + public function __construct() + { + parent::__construct(); + $this->count = 0; + $this->accountRepos = app(AccountRepositoryInterface::class); + $this->currencyRepos = app(CurrencyRepositoryInterface::class); + $this->journalRepos = app(JournalRepositoryInterface::class); + } + + /** + * Execute the console command. + * + * @return int + */ + public function handle(): int + { + $this->accountCurrencies = []; + + + $start = microtime(true); + // @codeCoverageIgnoreStart + if ($this->isExecuted() && true !== $this->option('force')) { + $this->warn('This command has already been executed.'); + + return 0; + } + // @codeCoverageIgnoreEnd + + $this->updateOtherJournalsCurrencies(); + $this->markAsExecuted(); + + if (0 === $this->count) { + $this->line('All transactions are correct.'); + } + if (0 !== $this->count) { + $this->line(sprintf('Verified %d transaction(s) and journal(s).', $this->count)); + } + $end = round(microtime(true) - $start, 2); + $this->info(sprintf('Verified and fixed transaction currencies in %s seconds.', $end)); + + return 0; + } + + /** + * @param Account $account + * + * @return TransactionCurrency|null + */ + private function getCurrency(Account $account): ?TransactionCurrency + { + $accountId = $account->id; + if (isset($this->accountCurrencies[$accountId]) && 0 === $this->accountCurrencies[$accountId]) { + return null; + } + if (isset($this->accountCurrencies[$accountId]) && $this->accountCurrencies[$accountId] instanceof TransactionCurrency) { + return $this->accountCurrencies[$accountId]; + } + $currencyId = (int)$this->accountRepos->getMetaValue($account, 'currency_id'); + $result = $this->currencyRepos->findNull($currencyId); + if (null === $result) { + $this->accountCurrencies[$accountId] = 0; + + return null; + } + $this->accountCurrencies[$accountId] = $result; + + return $result; + + + } + + /** + * @param TransactionJournal $journal + * + * @return Transaction|null + */ + private function getFirstAssetTransaction(TransactionJournal $journal): ?Transaction + { + $result = $journal->transactions->first( + static function (Transaction $transaction) { + // type can also be liability. + return AccountType::ASSET === $transaction->account->accountType->type; + } + ); + + return $result; + } + + + /** + * @return bool + */ + private function isExecuted(): bool + { + $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); + if (null !== $configVar) { + return (bool)$configVar->data; + } + + return false; // @codeCoverageIgnore + } + + /** + * + */ + private function markAsExecuted(): void + { + app('fireflyconfig')->set(self::CONFIG_NAME, true); + } + + /** + * @param TransactionJournal $journal + */ + private function updateJournalCurrency(TransactionJournal $journal): void + { + $leadTransaction = $this->getLeadTransaction($journal); + + if (null === $leadTransaction) { + $this->error(sprintf('Could not reliably determine which transaction is in the lead for transaction journal #%d.', $journal->id)); + + return; + } + + /** @var Account $account */ + $account = $leadTransaction->account; + $currency = $this->getCurrency($account); + if (null === $currency) { + $this->error(sprintf('Account #%d ("%s") has no currency preference, so transaction journal #%d can\'t be corrected', + $account->id, $account->name, $journal->id)); + + return; + } + // fix each transaction: + $journal->transactions->each( + static function (Transaction $transaction) use ($currency) { + if (null === $transaction->transaction_currency_id) { + $transaction->transaction_currency_id = $currency->id; + $transaction->save(); + $this->count++; + } + + // when mismatch in transaction: + if (!((int)$transaction->transaction_currency_id === (int)$currency->id)) { + $transaction->foreign_currency_id = (int)$transaction->transaction_currency_id; + $transaction->foreign_amount = $transaction->amount; + $transaction->transaction_currency_id = $currency->id; + $transaction->save(); + $this->count++; + } + } + ); + // also update the journal, of course: + $journal->transaction_currency_id = $currency->id; + $this->count++; + $journal->save(); + } + + /** + * This routine verifies that withdrawals, deposits and opening balances have the correct currency settings for + * the accounts they are linked to. + * + * Both source and destination must match the respective currency preference of the related asset account. + * So FF3 must verify all transactions. + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + private function updateOtherJournalsCurrencies(): void + { + $set = + $this->journalRepos->getAllJournals( + [ + TransactionType::WITHDRAWAL, + TransactionType::DEPOSIT, + TransactionType::OPENING_BALANCE, + TransactionType::RECONCILIATION, + ] + ); + + /** @var TransactionJournal $journal */ + foreach ($set as $journal) { + $this->updateJournalCurrency($journal); + } + } + + /** + * Gets the transaction that determines the transaction that "leads" and will determine + * the currency to be used by all transactions, and the journal itself. + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @param TransactionJournal $journal + * @return Transaction|null + */ + private function getLeadTransaction(TransactionJournal $journal): ?Transaction + { + /** @var Transaction $lead */ + $lead = null; + switch ($journal->transactionType->type) { + case TransactionType::WITHDRAWAL: + $lead = $journal->transactions()->where('amount', '<', 0)->first(); + break; + case TransactionType::DEPOSIT: + $lead = $journal->transactions()->where('amount', '>', 0)->first(); + break; + case TransactionType::OPENING_BALANCE: + // whichever isn't an initial balance account: + $lead = $journal->transactions() + ->leftJoin('accounts', 'transactions.account_id', '=', 'accounts.id') + ->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id') + ->where('account_types.type', '!=', AccountType::INITIAL_BALANCE) + ->first(['transactions.*']); + break; + case TransactionType::RECONCILIATION: + // whichever isn't the reconciliation account: + $lead = $journal->transactions() + ->leftJoin('accounts', 'transactions.account_id', '=', 'accounts.id') + ->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id') + ->where('account_types.type', '!=', AccountType::RECONCILIATION) + ->first(['transactions.*']); + break; + } + + return $lead; + } +} \ No newline at end of file diff --git a/app/Console/Commands/Upgrade/TransferCurrenciesCorrections.php b/app/Console/Commands/Upgrade/TransferCurrenciesCorrections.php new file mode 100644 index 0000000000..710a202725 --- /dev/null +++ b/app/Console/Commands/Upgrade/TransferCurrenciesCorrections.php @@ -0,0 +1,542 @@ +. + */ + +namespace FireflyIII\Console\Commands\Upgrade; + + +use FireflyIII\Models\Account; +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\TransactionType; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; +use FireflyIII\Repositories\Journal\JournalRepositoryInterface; +use Illuminate\Console\Command; +use Log; + +/** + * Class TransferCurrenciesCorrections + */ +class TransferCurrenciesCorrections extends Command +{ + + public const CONFIG_NAME = '4780_transfer_currencies'; + /** + * The console command description. + * + * @var string + */ + protected $description = 'Updates transfer currency information.'; + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'firefly-iii:transfer-currencies {--F|force : Force the execution of this command.}'; + /** @var array */ + private $accountCurrencies; + /** @var AccountRepositoryInterface */ + private $accountRepos; + /** @var CurrencyRepositoryInterface */ + private $currencyRepos; + /** @var JournalRepositoryInterface */ + private $journalRepos; + /** @var int */ + private $count; + + /** @var Transaction The source transaction of the current journal. */ + private $sourceTransaction; + /** @var Account The source account of the current journal. */ + private $sourceAccount; + /** @var TransactionCurrency The currency preference of the source account of the current journal. */ + private $sourceCurrency; + /** @var Transaction The destination transaction of the current journal. */ + private $destinationTransaction; + /** @var Account The destination account of the current journal. */ + private $destinationAccount; + /** @var TransactionCurrency The currency preference of the destination account of the current journal. */ + private $destinationCurrency; + + + /** + * JournalCurrencies constructor. + */ + public function __construct() + { + parent::__construct(); + $this->count = 0; + $this->accountRepos = app(AccountRepositoryInterface::class); + $this->currencyRepos = app(CurrencyRepositoryInterface::class); + $this->journalRepos = app(JournalRepositoryInterface::class); + $this->accountCurrencies = []; + $this->resetInformation(); + } + + /** + * Execute the console command. + * + * @return int + */ + public function handle(): int + { + $start = microtime(true); + // @codeCoverageIgnoreStart + if ($this->isExecuted() && true !== $this->option('force')) { + $this->warn('This command has already been executed.'); + + return 0; + } + // @codeCoverageIgnoreEnd + + $this->startUpdateRoutine(); + $this->markAsExecuted(); + + if (0 === $this->count) { + $this->line('All transfers have correct currency information.'); + } + if (0 !== $this->count) { + $this->line(sprintf('Verified currency information of %d transfer(s).', $this->count)); + } + $end = round(microtime(true) - $start, 2); + $this->info(sprintf('Verified and fixed currency information for transfers in %s seconds.', $end)); + + return 0; + } + + /** + * @param Account $account + * + * @return TransactionCurrency|null + */ + private function getCurrency(Account $account): ?TransactionCurrency + { + $accountId = $account->id; + if (isset($this->accountCurrencies[$accountId]) && 0 === $this->accountCurrencies[$accountId]) { + return null; + } + if (isset($this->accountCurrencies[$accountId]) && $this->accountCurrencies[$accountId] instanceof TransactionCurrency) { + return $this->accountCurrencies[$accountId]; + } + $currencyId = (int)$this->accountRepos->getMetaValue($account, 'currency_id'); + $result = $this->currencyRepos->findNull($currencyId); + if (null === $result) { + $this->accountCurrencies[$accountId] = 0; + + return null; + } + $this->accountCurrencies[$accountId] = $result; + + return $result; + + + } + + /** + * @param TransactionJournal $transfer + * + * @return Transaction|null + */ + private function getDestinationTransaction(TransactionJournal $transfer): ?Transaction + { + return $transfer->transactions->firstWhere('amount', '>', 0); + } + + /** + * @param TransactionJournal $transfer + * + * @return Transaction|null + */ + private function getSourceTransaction(TransactionJournal $transfer): ?Transaction + { + return $transfer->transactions->firstWhere('amount', '<', 0); + } + + /** + * @return bool + */ + private function isExecuted(): bool + { + $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); + if (null !== $configVar) { + return (bool)$configVar->data; + } + + return false; // @codeCoverageIgnore + } + + /** + * + */ + private function markAsExecuted(): void + { + app('fireflyconfig')->set(self::CONFIG_NAME, true); + } + + /** + * This method makes sure that the transaction journal uses the currency given in the source transaction. + * + * @param TransactionJournal $journal + */ + private function fixTransactionJournalCurrency(TransactionJournal $journal): void + { + if ($journal->transaction_currency_id !== $this->sourceCurrency->id) { + $oldCurrencyCode = $journal->transactionCurrency->code ?? '(nothing)'; + $journal->transaction_currency_id = $this->sourceCurrency->id; + $this->count++; + $this->line( + sprintf( + 'Transfer #%d ("%s") has been updated to use %s instead of %s.', + $journal->id, + $journal->description, + $this->sourceCurrency->code, + $oldCurrencyCode + ) + ); + $journal->save(); + } + } + + /** + * This routine verifies that transfers have the correct currency settings for the accounts they are linked to. + * For transfers, this is can be a destructive routine since we FORCE them into a currency setting whether they + * like it or not. Previous routines MUST have set the currency setting for both accounts for this to work. + * + * A transfer always has the + * + * Both source and destination must match the respective currency preference. So FF3 must verify ALL + * transactions. + */ + private function startUpdateRoutine(): void + { + $set = $this->journalRepos->getAllJournals([TransactionType::TRANSFER]); + /** @var TransactionJournal $journal */ + foreach ($set as $journal) { + $this->updateTransferCurrency($journal); + } + } + + /** + * Reset all the class fields for the current transfer + */ + private function resetInformation(): void + { + $this->sourceTransaction = null; + $this->sourceAccount = null; + $this->sourceCurrency = null; + $this->destinationTransaction = null; + $this->destinationAccount = null; + $this->destinationCurrency = null; + } + + /** + * Extract source transaction, source account + source account currency from the journal. + * @param TransactionJournal $journal + */ + private function getSourceInformation(TransactionJournal $journal): void + { + $this->sourceTransaction = $this->getSourceTransaction($journal); + $this->sourceAccount = null === $this->sourceTransaction ? null : $this->sourceTransaction->account; + $this->sourceCurrency = null === $this->sourceAccount ? null : $this->getCurrency($this->sourceAccount); + } + + /** + * Extract destination transaction, destination account + destination account currency from the journal. + * @param TransactionJournal $journal + */ + private function getDestinationInformation(TransactionJournal $journal): void + { + $this->destinationTransaction = $this->getDestinationTransaction($journal); + $this->destinationAccount = null === $this->destinationTransaction ? null : $this->destinationTransaction->account; + $this->destinationCurrency = null === $this->destinationAccount ? null : $this->getCurrency($this->destinationAccount); + } + + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * + * @param TransactionJournal $transfer + */ + private function updateTransferCurrency(TransactionJournal $transfer): void + { + $this->resetInformation(); + + if ($this->isSplitJournal($transfer)) { + $this->line(sprintf(sprintf('Transaction journal #%d is a split journal. Cannot continue.', $transfer->id))); + } + + $this->getSourceInformation($transfer); + $this->getDestinationInformation($transfer); + + // unexpectedly, either one is null: + if ($this->isEmptyTransactions()) { + $this->error(sprintf('Source or destination information for transaction journal #%d is null. Cannot fix this one.', $transfer->id)); + + return; + } + + // both accounts must have currency preference: + if ($this->isNoCurrencyPresent()) { + return; + } + + // fix source transaction having no currency. + $this->fixSourceNoCurrency(); + + // fix source transaction having bad currency. + $this->fixSourceUnmatchedCurrency(); + + // fix destination transaction having no currency. + $this->fixDestNoCurrency(); + + // fix destination transaction having bad currency. + $this->fixDestinationUnmatchedCurrency(); + + // remove foreign currency information if not necessary. + $this->fixInvalidForeignCurrency(); + + // correct foreign currency info if necessary. + $this->fixMismatchedForeignCurrency(); + + // restore missing foreign currency amount. + $this->fixSourceNullForeignAmount(); + $this->fixDestNullForeignAmount(); + + // fix journal itself: + $this->fixTransactionJournalCurrency($transfer); + } + + /** + * The source transaction must have a currency. If not, it will be added by + * taking it from the source account's preference. + */ + private function fixSourceNoCurrency(): void + { + if (null === $this->sourceTransaction->transaction_currency_id && null !== $this->sourceCurrency) { + $this->sourceTransaction + ->transaction_currency_id = (int)$this->sourceCurrency->id; + $message = sprintf('Transaction #%d has no currency setting, now set to %s.', + $this->sourceTransaction->id, $this->sourceCurrency->code); + Log::debug($message); + $this->line($message); + $this->count++; + $this->sourceTransaction->save(); + } + } + + /** + * The destination transaction must have a currency. If not, it will be added by + * taking it from the destination account's preference. + */ + private function fixDestNoCurrency(): void + { + if (null === $this->destinationTransaction->transaction_currency_id && null !== $this->destinationCurrency) { + $this->destinationTransaction + ->transaction_currency_id = (int)$this->destinationCurrency->id; + $message = sprintf('Transaction #%d has no currency setting, now set to %s.', + $this->destinationTransaction->id, $this->destinationCurrency->code); + Log::debug($message); + $this->line($message); + $this->count++; + $this->destinationTransaction->save(); + } + } + + /** + * The source transaction must have the correct currency. If not, it will be set by + * taking it from the source account's preference. + */ + private function fixSourceUnmatchedCurrency(): void + { + if (null !== $this->sourceCurrency && + null === $this->sourceTransaction->foreign_amount && + (int)$this->sourceTransaction->transaction_currency_id !== (int)$this->sourceCurrency->id + ) { + + + $message = sprintf( + 'Transaction #%d has a currency setting #%d that should be #%d. Amount remains %s, currency is changed.', + $this->sourceTransaction->id, + $this->sourceTransaction->transaction_currency_id, + $this->sourceAccount->id, + $this->sourceTransaction->amount + ); + Log::debug($message); + $this->line($message); + $this->count++; + $this->sourceTransaction->transaction_currency_id = (int)$this->sourceCurrency->id; + $this->sourceTransaction->save(); + } + } + + /** + * The destination transaction must have the correct currency. If not, it will be set by + * taking it from the destination account's preference. + */ + private function fixDestinationUnmatchedCurrency(): void + { + if (null !== $this->destinationCurrency && + null === $this->destinationTransaction->foreign_amount && + (int)$this->destinationTransaction->transaction_currency_id !== (int)$this->destinationCurrency->id + ) { + $message = sprintf( + 'Transaction #%d has a currency setting #%d that should be #%d. Amount remains %s, currency is changed.', + $this->destinationTransaction->id, + $this->destinationTransaction->transaction_currency_id, + $this->destinationAccount->id, + $this->destinationTransaction->amount + ); + Log::debug($message); + $this->line($message); + $this->count++; + $this->destinationTransaction->transaction_currency_id = (int)$this->destinationCurrency->id; + $this->destinationTransaction->save(); + } + } + + /** + * Is this a split transaction journal? + * + * @param TransactionJournal $transfer + * @return bool + */ + private function isSplitJournal(TransactionJournal $transfer): bool + { + return $transfer->transactions->count() > 2; + } + + /** + * Is either the source or destination transaction NULL? + * @return bool + */ + private function isEmptyTransactions(): bool + { + return null === $this->sourceTransaction || null === $this->destinationTransaction || + null === $this->sourceAccount || null === $this->destinationAccount; + } + + /** + * If the destination account currency is the same as the source currency, + * both foreign_amount and foreign_currency_id fields must be NULL + * for both transactions (because foreign currency info would not make sense) + */ + private function fixInvalidForeignCurrency(): void + { + if ((int)$this->destinationCurrency->id === (int)$this->sourceCurrency->id) { + // update both transactions to match: + $this->sourceTransaction->foreign_amount = null; + $this->sourceTransaction->foreign_currency_id = null; + + $this->destinationTransaction->foreign_amount = null; + $this->destinationTransaction->foreign_currency_id = null; + + $this->sourceTransaction->save(); + $this->destinationTransaction->save(); + + Log::debug( + sprintf( + 'Currency for account "%s" is %s, and currency for account "%s" is also + %s, so transactions #%d and #%d has been verified to be to %s exclusively.', + $this->destinationAccount->name, $this->destinationCurrency->code, + $this->sourceAccount->name, $this->sourceCurrency->code, + $this->sourceTransaction->id, $this->destinationTransaction->id, $this->sourceCurrency->code + ) + ); + $this->count++; + + return; + } + } + + /** + * If destination account currency is different from source account currency, then + * both transactions must have each others currency as foreign currency id. + */ + private function fixMismatchedForeignCurrency(): void + { + if ((int)$this->sourceCurrency->id !== (int)$this->destinationCurrency->id) { + $this->sourceTransaction->foreign_currency_id = $this->destinationCurrency->id; + $this->destinationTransaction->foreign_currency_id = $this->sourceCurrency->id; + + $this->sourceTransaction->save(); + $this->destinationTransaction->save(); + $this->count++; + Log::debug(sprintf('Verified foreign currency ID of transaction #%d and #%d', $this->sourceTransaction->id, $this->destinationTransaction->id)); + } + } + + /** + * If the foreign amount of the source transaction is null, but that of the other isn't, use this piece of code + * to restore it. + */ + private function fixSourceNullForeignAmount(): void + { + if (null === $this->sourceTransaction->foreign_amount && null !== $this->destinationTransaction->foreign_amount) { + $this->sourceTransaction->foreign_amount = bcmul((string)$this->destinationTransaction->foreign_amount, '-1'); + $this->sourceTransaction->save(); + $this->count++; + Log::debug(sprintf('Restored foreign amount of source transaction #%d to %s', + $this->sourceTransaction->id, $this->sourceTransaction->foreign_amount)); + } + } + + /** + * If the foreign amount of the destination transaction is null, but that of the other isn't, use this piece of code + * to restore it. + */ + private function fixDestNullForeignAmount(): void + { + if (null === $this->destinationTransaction->foreign_amount && null !== $this->sourceTransaction->foreign_amount) { + $this->destinationTransaction->foreign_amount = bcmul((string)$this->sourceTransaction->foreign_amount, '-1'); + $this->destinationTransaction->save(); + $this->count++; + Log::debug(sprintf('Restored foreign amount of destination transaction #%d to %s', + $this->destinationTransaction->id, $this->destinationTransaction->foreign_amount)); + } + } + + /** + * @return bool + */ + private function isNoCurrencyPresent(): bool + { + // source account must have a currency preference. + if (null === $this->sourceCurrency) { + $message = sprintf('Account #%d ("%s") must have currency preference but has none.', $this->sourceAccount->id, $this->sourceAccount->name); + Log::error($message); + $this->error($message); + + return false; + } + + // destination account must have a currency preference. + if (null === $this->destinationCurrency) { + $message = sprintf('Account #%d ("%s") must have currency preference but has none.', + $this->destinationAccount->id, $this->destinationAccount->name); + Log::error($message); + $this->error($message); + + return false; + } + + return true; + } + +} \ No newline at end of file