From 200a4b18a81b3e02db324b76a44f75fa6e93f3a8 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 17 Mar 2019 17:05:16 +0100 Subject: [PATCH] First full implementation of new storage routine. --- app/Console/Commands/MigrateToGroups.php | 286 --------- .../Commands/Upgrade/MigrateToGroups.php | 313 ++++++++++ app/Factory/AccountFactory.php | 8 + app/Factory/TransactionFactory.php | 141 ++--- app/Factory/TransactionJournalFactory.php | 565 +++++++++--------- app/Repositories/Bill/BillRepository.php | 39 ++ .../Bill/BillRepositoryInterface.php | 11 + app/Repositories/Budget/BudgetRepository.php | 61 +- .../Budget/BudgetRepositoryInterface.php | 19 + .../Category/CategoryRepository.php | 49 +- .../Category/CategoryRepositoryInterface.php | 9 + .../Currency/CurrencyRepository.php | 8 + .../PiggyBank/PiggyBankRepository.php | 37 ++ .../PiggyBankRepositoryInterface.php | 10 +- .../TransactionTypeRepository.php | 6 +- app/Support/NullArrayObject.php | 58 ++ .../v1/accounts/reconcile/transactions.twig | 2 +- resources/views/v1/partials/journal-row.twig | 2 +- resources/views/v1/transactions/show.twig | 8 +- 19 files changed, 958 insertions(+), 674 deletions(-) delete mode 100644 app/Console/Commands/MigrateToGroups.php create mode 100644 app/Console/Commands/Upgrade/MigrateToGroups.php create mode 100644 app/Support/NullArrayObject.php diff --git a/app/Console/Commands/MigrateToGroups.php b/app/Console/Commands/MigrateToGroups.php deleted file mode 100644 index c835f949c4..0000000000 --- a/app/Console/Commands/MigrateToGroups.php +++ /dev/null @@ -1,286 +0,0 @@ -journalFactory = app(TransactionJournalFactory::class); - $this->journalRepository = app(JournalRepositoryInterface::class); - } - - /** - * Execute the console command. - * - * @return mixed - */ - public function handle() - { - if ($this->isMigrated()) { - $this->info('Database already seems to be migrated.'); - } - Log::debug('---- start group migration ----'); - $this->makeGroups(); - - Log::debug('---- end group migration ----'); - - return 0; - } - - /** - * @return bool - */ - private function isMigrated(): bool - { - $configName = 'migrated_to_groups_478'; - $configVar = app('fireflyconfig')->get($configName, false); - if (null !== $configVar) { - return (bool)$configVar->data; - } - - return false; - } - - /** - * @param TransactionJournal $journal - * - * @throws \FireflyIII\Exceptions\FireflyException - */ - private function makeGroup(TransactionJournal $journal): void - { - // double check transaction count. - if ($journal->transactions->count() <= 2) { - return; - } - $this->journalRepository->setUser($journal->user); - $this->journalFactory->setUser($journal->user); - - $data = [ - // mandatory fields. - 'type' => strtolower($journal->transactionType->type), - 'date' => $journal->date, - 'user' => $journal->user_id, - 'description' => $journal->description, - - // transactions: - 'transactions' => [], - ]; - - - // simply use the positive transactions as a base to create new transaction journals. - $transactions = $journal->transactions()->where('amount', '>', 0)->get(); - /** @var Transaction $transaction */ - foreach ($transactions as $transaction) { - $budgetId = 0; - $categoryId = 0; - if (null !== $transaction->budgets()->first()) { - $budgetId = $transaction->budgets()->first()->id; - } - if (null !== $transaction->categories()->first()) { - $categoryId = $transaction->categories()->first()->id; - } - // opposing for source: - /** @var Transaction $opposing */ - $opposing = $journal->transactions()->where('amount', $transaction->amount * -1) - ->where('identifier', $transaction->identifier)->first(); - if (null === $opposing) { - $this->error(sprintf('Could not convert journal #%d', $journal->id)); - - return; - } - - $tArray = [ - - // currency and foreign currency - 'currency' => null, - 'currency_id' => $transaction->transaction_currency_id, - 'currency_code' => null, - 'foreign_currency' => null, - 'foreign_currency_id' => $transaction->foreign_currency_id, - 'foreign_currency_code' => null, - - // amount and foreign amount - 'amount' => $transaction->amount, - 'foreign_amount' => $transaction->foreign_amount, - - // description - 'description' => $transaction->description, - - // source - 'source' => null, - 'source_id' => $opposing->account_id, - 'source_name' => null, - - // destination - 'destination' => null, - 'destination_id' => $transaction->account_id, - 'destination_name' => null, - - // budget - 'budget' => null, - 'budget_id' => $budgetId, - 'budget_name' => null, - - // category - 'category' => null, - 'category_id' => $categoryId, - 'category_name' => null, - - // piggy bank (if transfer) - 'piggy_bank' => null, - 'piggy_bank_id' => null, - 'piggy_bank_name' => null, - - // bill (if withdrawal) - 'bill' => null, - 'bill_id' => $journal->bill_id, - 'bill_name' => null, - - // some other interesting properties - 'reconciled' => false, - 'notes' => $this->journalRepository->getNoteText($journal), - 'tags' => $journal->tags->pluck('tag')->toArray(), - - // all custom fields: - 'internal_reference' => $this->journalRepository->getMetaField($journal, 'internal-reference'), - 'sepa-cc' => $this->journalRepository->getMetaField($journal, 'sepa-cc'), - 'sepa-ct-op' => $this->journalRepository->getMetaField($journal, 'sepa-ct-op'), - 'sepa-ct-id' => $this->journalRepository->getMetaField($journal, 'sepa-ct-id'), - 'sepa-db' => $this->journalRepository->getMetaField($journal, 'sepa-db'), - 'sepa-country' => $this->journalRepository->getMetaField($journal, 'sepa-country'), - 'sepa-ep' => $this->journalRepository->getMetaField($journal, 'sepa-ep'), - 'sepa-ci' => $this->journalRepository->getMetaField($journal, 'sepa-ci'), - 'sepa-batch-id' => $this->journalRepository->getMetaField($journal, 'sepa-batch-id'), - 'interest_date' => $this->journalRepository->getMetaDate($journal, 'interest_date'), - 'book_date' => $this->journalRepository->getMetaDate($journal, 'book_date'), - 'process_date' => $this->journalRepository->getMetaDate($journal, 'process_date'), - 'due_date' => $this->journalRepository->getMetaDate($journal, 'due_date'), - 'payment_date' => $this->journalRepository->getMetaDate($journal, 'payment_date'), - 'invoice_date' => $this->journalRepository->getMetaDate($journal, 'invoice_date'), - 'external_id' => $this->journalRepository->getMetaField($journal, 'external-id'), - 'original-source' => $this->journalRepository->getMetaField($journal, 'original-source'), - 'recurrence_id' => $this->journalRepository->getMetaField($journal, 'recurrence_id'), - 'bunq_payment_id' => $this->journalRepository->getMetaField($journal, 'bunq_payment_id'), - 'importHash' => $this->journalRepository->getMetaField($journal, 'importHash'), - 'importHashV2' => $this->journalRepository->getMetaField($journal, 'importHashV2'), - ]; - $data['transactions'][] = $tArray; - } - $result = $this->journalFactory->create($data); - // create a new transaction journal based on this particular transaction using the factory. - // delete the old transaction journal. - //$journal->delete(); - Log::debug(sprintf('Migrated journal #%d into %s', $journal->id, implode(', ', $result->pluck('id')->toArray()))); - } - - /** - * - */ - private function makeGroups(): void - { - - // grab all split transactions: - $all = Transaction::groupBy('transaction_journal_id') - ->get(['transaction_journal_id', DB::raw('COUNT(transaction_journal_id) as result')]); - /** @var Collection $filtered */ - $filtered = $all->filter( - function (Transaction $transaction) { - return $transaction->result > 2; - } - ); - $journalIds = array_unique($filtered->pluck('transaction_journal_id')->toArray()); - $splitJournals = TransactionJournal::whereIn('id', $journalIds)->get(); - $this->info(sprintf('Going to un-split %d transactions. This could take some time.', $splitJournals->count())); - - /** @var TransactionJournal $journal */ - foreach ($splitJournals as $journal) { - $group = $this->makeGroup($journal); - } - - return; - - // first run, create new transaction journals and groups for splits - /** @var TransactionJournal $journal */ - foreach ($journals as $journal) { - Log::debug(sprintf('Now going to migrate journal #%d', $journal->id)); - //$this->migrateCategory($journal); - //$this->migrateBudget($journal); - } - - } - - /** - * Migrate the category. This is basically a back-and-forth between the journal - * and underlying categories. - * - * @param TransactionJournal $journal - */ - private function migrateCategory(TransactionJournal $journal): void - { - /** @var Category $category */ - $category = $journal->categories()->first(); - $transactions = $journal->transactions; - $tCategory = null; - Log::debug(sprintf('Journal #%d has %d transactions', $journal->id, $transactions->count())); - - /** @var Transaction $transaction */ - foreach ($transactions as $transaction) { - $tCategory = $tCategory ?? $transaction->categories()->first(); - // category and tCategory are null. - if (null === $category && null === $tCategory) { - Log::debug(sprintf('Transaction #%d and journal #%d both have no category set. Continue.', $transaction->id, $journal->id)); - continue; - } - // category is null, tCategory is not. - if (null === $category && null !== $tCategory) { - Log::debug(sprintf('Transaction #%d has a category but journal #%d does not. Will update journal.', $transaction->id, $journal->id)); - $journal->categories()->save($tCategory); - $category = $tCategory; - continue; - } - // tCategory is null, category is not. - - // tCategory and category are equal - // tCategory and category are not equal - } - } -} diff --git a/app/Console/Commands/Upgrade/MigrateToGroups.php b/app/Console/Commands/Upgrade/MigrateToGroups.php new file mode 100644 index 0000000000..91e6bb3415 --- /dev/null +++ b/app/Console/Commands/Upgrade/MigrateToGroups.php @@ -0,0 +1,313 @@ +. + */ + +namespace FireflyIII\Console\Commands\Upgrade; + +use Carbon\Carbon; +use DB; +use Exception; +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Factory\TransactionJournalFactory; +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Repositories\Journal\JournalRepositoryInterface; +use FireflyIII\Services\Internal\Destroy\JournalDestroyService; +use Illuminate\Console\Command; +use Illuminate\Support\Collection; +use Log; + +/** + * This command will take split transactions and migrate them to "transaction groups". + * + * It will only run once, but can be forced to run again. + * + * Class MigrateToGroups + */ +class MigrateToGroups extends Command +{ + /** + * The console command description. + * + * @var string + */ + protected $description = 'Migrates a pre-4.7.8 transaction structure to the 4.7.8+ transaction structure.'; + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'firefly:migrate-to-groups {--F|force : Force the migration, even if it fired before.}'; + + /** @var TransactionJournalFactory */ + private $journalFactory; + + /** @var JournalRepositoryInterface */ + private $journalRepository; + + /** + * Create a new command instance. + * + * @return void + */ + public function __construct() + { + parent::__construct(); + $this->journalFactory = app(TransactionJournalFactory::class); + $this->journalRepository = app(JournalRepositoryInterface::class); + } + + /** + * @param TransactionJournal $journal + * @param Transaction $transaction + * + * @return int + */ + public function getBudgetId(TransactionJournal $journal, Transaction $transaction): int + { + $budgetId = 0; + if (null !== $transaction->budgets()->first()) { + $budgetId = (int)$transaction->budgets()->first()->id; // done! + Log::debug(sprintf('Transaction #%d has a reference to budget #%d so will use that one.', $transaction->id, $budgetId)); + } + if (0 === $budgetId && $journal->budgets()->first()) { + $budgetId = (int)$journal->budgets()->first()->id; // also done! + Log::debug( + sprintf('Transaction #%d has NO budget, but journal #%d has budget #%d so will use that one.', $transaction->id, $journal->id, $budgetId) + ); + } + Log::debug(sprintf('Final budget ID for journal #%d and transaction #%d is %d', $journal->id, $transaction->id, $budgetId)); + + return $budgetId; + + } + + /** + * @param TransactionJournal $journal + * @param Transaction $transaction + * + * @return int + */ + public function getCategoryId(TransactionJournal $journal, Transaction $transaction): int + { + $categoryId = 0; + if (null !== $transaction->categories()->first()) { + $categoryId = (int)$transaction->categories()->first()->id; // done! + Log::debug(sprintf('Transaction #%d has a reference to category #%d so will use that one.', $transaction->id, $categoryId)); + } + if (0 === $categoryId && $journal->categories()->first()) { + $categoryId = (int)$journal->categories()->first()->id; // also done! + Log::debug( + sprintf('Transaction #%d has NO category, but journal #%d has category #%d so will use that one.', $transaction->id, $journal->id, $categoryId) + ); + } + Log::debug(sprintf('Final category ID for journal #%d and transaction #%d is %d', $journal->id, $transaction->id, $categoryId)); + + return $categoryId; + + } + + /** + * @param TransactionJournal $journal + * @param Transaction $transaction + * + * @return Transaction + * @throws FireflyException + */ + public function getOpposingTransaction(TransactionJournal $journal, Transaction $transaction): Transaction + { + /** @var Transaction $opposing */ + $opposing = $journal->transactions()->where('amount', $transaction->amount * -1)->where('identifier', $transaction->identifier)->first(); + if (null === $opposing) { + $message = sprintf( + 'Could not convert journal #%d ("%s") because transaction #%d has no opposite entry in the database. This requires manual intervention beyond the capabilities of this script. Please open an issue on GitHub.', + $journal->id, $journal->description, $transaction->id + ); + $this->error($message); + throw new FireflyException($message); + } + Log::debug(sprintf('Found opposing transaction #%d for transaction #%d (both part of journal #%d)', $opposing->id, $transaction->id, $journal->id)); + + return $opposing; + } + + /** + * Execute the console command. + * + * @return int + * @throws Exception + */ + public function handle(): int + { + if ($this->isMigrated() && true !== $this->option('force')) { + $this->info('Database already seems to be migrated.'); + } + if (true === $this->option('force')) { + $this->warn('Forcing the migration.'); + } + + Log::debug('---- start group migration ----'); + $this->makeGroups(); + Log::debug('---- end group migration ----'); + + $this->markAsMigrated(); + + return 0; + } + + /** + * @return bool + */ + private function isMigrated(): bool + { + $configName = 'migrated_to_groups_4780'; + $configVar = app('fireflyconfig')->get($configName, false); + if (null !== $configVar) { + return (bool)$configVar->data; + } + + return false; + } + + /** + * @param TransactionJournal $journal + * + * @throws Exception + */ + private function makeGroup(TransactionJournal $journal): void + { + // double check transaction count. + if ($journal->transactions->count() <= 2) { + Log::debug(sprintf('Will not try to convert journal #%d because it has 2 or less transactions.', $journal->id)); + + return; + } + Log::debug(sprintf('Will now try to convert journal #%d', $journal->id)); + + $this->journalRepository->setUser($journal->user); + $this->journalFactory->setUser($journal->user); + + /** @var JournalDestroyService $service */ + $service = app(JournalDestroyService::class); + + $data = [ + // mandatory fields. + 'type' => strtolower($journal->transactionType->type), + 'date' => $journal->date, + 'user' => $journal->user_id, + 'group_title' => $journal->description, + 'transactions' => [], + ]; + + $transactions = $journal->transactions()->where('amount', '>', 0)->get(); + Log::debug(sprintf('Will use %d positive transactions to create a new group.', $transactions->count())); + + /** @var Transaction $transaction */ + foreach ($transactions as $transaction) { + Log::debug(sprintf('Now going to add transaction #%d to the array.', $transaction->id)); + $budgetId = $this->getBudgetId($journal, $transaction); + $categoryId = $this->getCategoryId($journal, $transaction); + $opposingTr = $this->getOpposingTransaction($journal, $transaction); + $tArray = [ + + // currency and foreign currency + 'currency_id' => $transaction->transaction_currency_id, + 'foreign_currency_id' => $transaction->foreign_currency_id, + 'amount' => $transaction->amount, + 'foreign_amount' => $transaction->foreign_amount, + 'description' => $transaction->description ?? $journal->description, + 'source_id' => $opposingTr->account_id, + 'destination_id' => $transaction->account_id, + 'budget_id' => $budgetId, + 'category_id' => $categoryId, + 'bill_id' => $journal->bill_id, + 'notes' => $this->journalRepository->getNoteText($journal), + 'tags' => $journal->tags->pluck('tag')->toArray(), + 'internal_reference' => $this->journalRepository->getMetaField($journal, 'internal-reference'), + 'sepa-cc' => $this->journalRepository->getMetaField($journal, 'sepa-cc'), + 'sepa-ct-op' => $this->journalRepository->getMetaField($journal, 'sepa-ct-op'), + 'sepa-ct-id' => $this->journalRepository->getMetaField($journal, 'sepa-ct-id'), + 'sepa-db' => $this->journalRepository->getMetaField($journal, 'sepa-db'), + 'sepa-country' => $this->journalRepository->getMetaField($journal, 'sepa-country'), + 'sepa-ep' => $this->journalRepository->getMetaField($journal, 'sepa-ep'), + 'sepa-ci' => $this->journalRepository->getMetaField($journal, 'sepa-ci'), + 'sepa-batch-id' => $this->journalRepository->getMetaField($journal, 'sepa-batch-id'), + 'external_id' => $this->journalRepository->getMetaField($journal, 'external-id'), + 'original-source' => $this->journalRepository->getMetaField($journal, 'original-source'), + 'recurrence_id' => $this->journalRepository->getMetaField($journal, 'recurrence_id'), + 'bunq_payment_id' => $this->journalRepository->getMetaField($journal, 'bunq_payment_id'), + 'importHash' => $this->journalRepository->getMetaField($journal, 'importHash'), + 'importHashV2' => $this->journalRepository->getMetaField($journal, 'importHashV2'), + 'interest_date' => $this->journalRepository->getMetaDate($journal, 'interest_date'), + 'book_date' => $this->journalRepository->getMetaDate($journal, 'book_date'), + 'process_date' => $this->journalRepository->getMetaDate($journal, 'process_date'), + 'due_date' => $this->journalRepository->getMetaDate($journal, 'due_date'), + 'payment_date' => $this->journalRepository->getMetaDate($journal, 'payment_date'), + 'invoice_date' => $this->journalRepository->getMetaDate($journal, 'invoice_date'), + ]; + + $data['transactions'][] = $tArray; + } + Log::debug(sprintf('Now calling transaction journal factory (%d transactions in array)', count($data['transactions']))); + $result = $this->journalFactory->create($data); + Log::debug('Done calling transaction journal factory'); + + // delete the old transaction journal. + //$service->destroy($journal); + + // report on result: + Log::debug(sprintf('Migrated journal #%d into these journals: %s', $journal->id, implode(', ', $result->pluck('id')->toArray()))); + $this->line(sprintf('Migrated journal #%d into these journals: %s', $journal->id, implode(', ', $result->pluck('id')->toArray()))); + } + + /** + * + * @throws Exception + */ + private function makeGroups(): void + { + // grab all split transactions: + $all = Transaction::groupBy('transaction_journal_id')->get(['transaction_journal_id', DB::raw('COUNT(transaction_journal_id) as result')]); + /** @var Collection $filtered */ + $filtered = $all->filter( + function (Transaction $transaction) { + return (int)$transaction->result > 2; + } + ); + $journalIds = array_unique($filtered->pluck('transaction_journal_id')->toArray()); + $splitJournals = TransactionJournal::whereIn('id', $journalIds)->get(); + if ($splitJournals->count() > 0) { + $this->info(sprintf('Going to un-split %d transaction(s). This could take some time.', $splitJournals->count())); + /** @var TransactionJournal $journal */ + foreach ($splitJournals as $journal) { + $this->makeGroup($journal); + } + } + } + + /** + * + */ + private function markAsMigrated(): void + { + app('fireflyconfig')->set('migrated_to_groups_4780', true); + } + +} diff --git a/app/Factory/AccountFactory.php b/app/Factory/AccountFactory.php index ccb86beda8..934f211b34 100644 --- a/app/Factory/AccountFactory.php +++ b/app/Factory/AccountFactory.php @@ -80,6 +80,7 @@ class AccountFactory $data['iban'] = $this->filterIban($data['iban']); // account may exist already: + Log::debug('Data array is as follows', $data); $return = $this->find($data['name'], $type->type); if (null === $return) { @@ -237,6 +238,13 @@ class AccountFactory $result = AccountType::whereType($accountType)->first(); } } + if (null === $result) { + Log::warning(sprintf('Found NO account type based on %d and "%s"', $accountTypeId, $accountType)); + } + if (null !== $result) { + Log::debug(sprintf('Found account type based on %d and "%s": "%s"', $accountTypeId, $accountType, $result->type)); + } + return $result; diff --git a/app/Factory/TransactionFactory.php b/app/Factory/TransactionFactory.php index 8dd3a3cd40..b9803457d5 100644 --- a/app/Factory/TransactionFactory.php +++ b/app/Factory/TransactionFactory.php @@ -33,6 +33,7 @@ use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Support\NullArrayObject; use FireflyIII\User; use Illuminate\Support\Collection; use Log; @@ -97,104 +98,42 @@ class TransactionFactory } /** - * @param TransactionCurrency $currency - * @param array $data + * @param NullArrayObject $data + * @param TransactionCurrency $currency + * @param TransactionCurrency|null $foreignCurrency * * @return Collection * @throws FireflyException */ - public function createPair(TransactionCurrency $currency, array $data): Collection + public function createPair(NullArrayObject $data, TransactionCurrency $currency, ?TransactionCurrency $foreignCurrency): Collection { $sourceAccount = $this->getAccount('source', $data['source'], $data['source_id'], $data['source_name']); $destinationAccount = $this->getAccount('destination', $data['destination'], $data['destination_id'], $data['destination_name']); $amount = $this->getAmount($data['amount']); + $foreignAmount = $this->getForeignAmount($data['foreign_amount']); $one = $this->create($sourceAccount, $currency, app('steam')->negative($amount)); $two = $this->create($destinationAccount, $currency, app('steam')->positive($amount)); + $one->reconciled = $data['reconciled'] ?? false; + $two->reconciled = $data['reconciled'] ?? false; + + // add foreign currency info to $one and $two if necessary. + if (null !== $foreignCurrency) { + $one->foreign_currency_id = $foreignCurrency->id; + $two->foreign_currency_id = $foreignCurrency->id; + $one->foreign_amount = $foreignAmount; + $two->foreign_amount = $foreignAmount; + } + + + $one->save(); + $two->save(); + return new Collection([$one, $two]); - // Log::debug('Start of TransactionFactory::createPair()' ); - // - // // type of source account and destination account depends on journal type: - // $sourceType = $this->accountType($journal, 'source'); - // $destinationType = $this->accountType($journal, 'destination'); - // - // Log::debug(sprintf('Journal is a %s.', $journal->transactionType->type)); - // Log::debug(sprintf('Expect source account to be of type "%s"', $sourceType)); - // Log::debug(sprintf('Expect source destination to be of type "%s"', $destinationType)); - // - // // find source and destination account: - // $sourceAccount = $this->findAccount($sourceType, $data['source'], (int)$data['source_id'], $data['source_name']); - // $destinationAccount = $this->findAccount($destinationType, $data['destination'], (int)$data['destination_id'], $data['destination_name']); - // - // if (null === $sourceAccount || null === $destinationAccount) { - // $debugData = $data; - // $debugData['source_type'] = $sourceType; - // $debugData['dest_type'] = $destinationType; - // Log::error('Info about source/dest:', $debugData); - // throw new FireflyException('Could not determine source or destination account.'); - // } - // - // Log::debug(sprintf('Source type is "%s", destination type is "%s"', $sourceAccount->accountType->type, $destinationAccount->accountType->type)); - // - // // based on the source type, destination type and transaction type, the system can start throwing FireflyExceptions. - // $this->validateTransaction($sourceAccount->accountType->type, $destinationAccount->accountType->type, $journal->transactionType->type); - // $source = $this->create( - // [ - // 'description' => null, - // 'amount' => app('steam')->negative((string)$data['amount']), - // 'foreign_amount' => $data['foreign_amount'] ? app('steam')->negative((string)$data['foreign_amount']): null, - // 'currency' => $data['currency'], - // 'foreign_currency' => $data['foreign_currency'], - // 'account' => $sourceAccount, - // 'transaction_journal' => $journal, - // 'reconciled' => $data['reconciled'], - // ] - // ); - // $dest = $this->create( - // [ - // 'description' => null, - // 'amount' => app('steam')->positive((string)$data['amount']), - // 'foreign_amount' => $data['foreign_amount'] ? app('steam')->positive((string)$data['foreign_amount']): null, - // 'currency' => $data['currency'], - // 'foreign_currency' => $data['foreign_currency'], - // 'account' => $destinationAccount, - // 'transaction_journal' => $journal, - // 'reconciled' => $data['reconciled'], - // ] - // ); - // if (null === $source || null === $dest) { - // throw new FireflyException('Could not create transactions.'); // @codeCoverageIgnore - // } - // - // return new Collection([$source, $dest]); } - // /** - // * @param array $data - // * - // * @return Transaction - // */ - // public function create(array $data): ?Transaction - // { - // $data['foreign_amount'] = '' === (string)$data['foreign_amount'] ? null : $data['foreign_amount']; - // Log::debug(sprintf('Create transaction for account #%d ("%s") with amount %s', $data['account']->id, $data['account']->name, $data['amount'])); - // - // return Transaction::create( - // [ - // 'reconciled' => $data['reconciled'], - // 'account_id' => $data['account']->id, - // 'transaction_journal_id' => $data['transaction_journal']->id, - // 'description' => $data['description'], - // 'transaction_currency_id' => $data['currency']->id, - // 'amount' => $data['amount'], - // 'foreign_amount' => $data['foreign_amount'], - // 'foreign_currency_id' => $data['foreign_currency'] ? $data['foreign_currency']->id : null, - // 'identifier' => 0, - // ] - // ); - // } /** * @param TransactionJournal $journal @@ -224,6 +163,8 @@ class TransactionFactory */ private function getAccount(string $direction, ?Account $source, ?int $sourceId, ?string $sourceName): Account { + Log::debug(sprintf('Now in getAccount(%s)', $direction)); + Log::debug(sprintf('Parameters: ((account), %s, %s)', var_export($sourceId, true), var_export($sourceName, true))); // expected type of source account, in order of preference $array = [ 'source' => [ @@ -252,12 +193,11 @@ class TransactionFactory $transactionType = $this->journal->transactionType->type; Log::debug( sprintf( - 'Based on the fact that the transaction is a %s, the %s account should be in %s', $transactionType, $direction, + 'Based on the fact that the transaction is a %s, the %s account should be in: %s', $transactionType, $direction, implode(', ', $expectedTypes[$transactionType]) ) ); - // first attempt, check the "source" object. if (null !== $source && $source->user_id === $this->user->id && \in_array($source->accountType->type, $expectedTypes[$transactionType], true)) { Log::debug(sprintf('Found "account" object for %s: #%d, %s', $direction, $source->id, $source->name)); @@ -268,6 +208,10 @@ class TransactionFactory // second attempt, find by ID. if (null !== $sourceId) { $source = $this->accountRepository->findNull($sourceId); + if (null !== $source) { + Log::debug(sprintf('Found account #%d ("%s" of type "%s") based on #%d.', $source->id, $source->name, $source->accountType->type, $sourceId)); + } + if (null !== $source && \in_array($source->accountType->type, $expectedTypes[$transactionType], true)) { Log::debug(sprintf('Found "account_id" object for %s: #%d, %s', $direction, $source->id, $source->name)); @@ -288,7 +232,7 @@ class TransactionFactory return $source; } } - + $sourceName = $sourceName ?? '(no name)'; // final attempt, create it. $preferredType = $expectedTypes[$transactionType][0]; if (AccountType::ASSET === $preferredType) { @@ -323,6 +267,33 @@ class TransactionFactory return $amount; } + + /** + * @param string|null $amount + * + * @return string + */ + private function getForeignAmount(?string $amount): ?string + { + if (null === $amount) { + Log::debug('No foreign amount info in array. Return NULL'); + + return null; + } + if ('' === $amount) { + Log::debug('Foreign amount is empty string, return NULL.'); + + return null; + } + if (0 === bccomp('0', $amount)) { + Log::debug('Foreign amount is 0.0, return NULL.'); + + return null; + } + Log::debug(sprintf('Foreign amount is %s', $amount)); + + return $amount; + } // // /** // * @param string $sourceType diff --git a/app/Factory/TransactionJournalFactory.php b/app/Factory/TransactionJournalFactory.php index c88a55170d..2d69cad86a 100644 --- a/app/Factory/TransactionJournalFactory.php +++ b/app/Factory/TransactionJournalFactory.php @@ -26,48 +26,69 @@ namespace FireflyIII\Factory; use Carbon\Carbon; use Exception; +use FireflyIII\Models\Note; +use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Models\TransactionGroup; use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\TransactionType; +use FireflyIII\Repositories\Bill\BillRepositoryInterface; +use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; +use FireflyIII\Repositories\Category\CategoryRepositoryInterface; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; +use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface; use FireflyIII\Repositories\TransactionType\TransactionTypeRepositoryInterface; +use FireflyIII\Support\NullArrayObject; use FireflyIII\User; use Illuminate\Support\Collection; use Log; +use function nspl\ds\defaultarray; /** * Class TransactionJournalFactory */ class TransactionJournalFactory { + /** @var BillRepositoryInterface */ + private $billRepository; + /** @var BudgetRepositoryInterface */ + private $budgetRepository; + /** @var CategoryRepositoryInterface */ + private $categoryRepository; /** @var CurrencyRepositoryInterface */ private $currencyRepository; - + /** @var array */ + private $fields; + /** @var PiggyBankRepositoryInterface */ + private $piggyRepository; /** @var TransactionFactory */ private $transactionFactory; - /** @var TransactionTypeRepositoryInterface */ private $typeRepository; - - // private $fields; /** @var User The user */ private $user; - // - // use JournalServiceTrait, TransactionTypeTrait; - /** * Constructor. + * + * @throws Exception */ public function __construct() { - // $this->fields = ['sepa-cc', 'sepa-ct-op', 'sepa-ct-id', 'sepa-db', 'sepa-country', 'sepa-ep', 'sepa-ci', 'interest_date', 'book_date', 'process_date', - // 'due_date', 'recurrence_id', 'payment_date', 'invoice_date', 'internal_reference', 'bunq_payment_id', 'importHash', 'importHashV2', - // 'external_id', 'sepa-batch-id', 'original-source']; + $this->fields = ['sepa-cc', 'sepa-ct-op', 'sepa-ct-id', 'sepa-db', 'sepa-country', 'sepa-ep', 'sepa-ci', 'interest_date', 'book_date', 'process_date', + 'due_date', 'recurrence_id', 'payment_date', 'invoice_date', 'internal_reference', 'bunq_payment_id', 'importHash', 'importHashV2', + 'external_id', 'sepa-batch-id', 'original-source']; + + if ('testing' === config('app.env')) { Log::warning(sprintf('%s should not be instantiated in the TEST environment!', \get_class($this))); } $this->currencyRepository = app(CurrencyRepositoryInterface::class); $this->typeRepository = app(TransactionTypeRepositoryInterface::class); $this->transactionFactory = app(TransactionFactory::class); + $this->billRepository = app(BillRepositoryInterface::class); + $this->budgetRepository = app(BudgetRepositoryInterface::class); + $this->categoryRepository = app(CategoryRepositoryInterface::class); + $this->piggyRepository = app(PiggyBankRepositoryInterface::class); } /** @@ -84,22 +105,36 @@ class TransactionJournalFactory $collection = new Collection; $transactions = $data['transactions'] ?? []; $type = $this->typeRepository->findTransactionType(null, $data['type']); - $description = app('steam')->cleanString($data['description']); $carbon = $data['date'] ?? new Carbon; $carbon->setTimezone(config('app.timezone')); - /** @var array $transaction */ - foreach ($transactions as $transaction) { + Log::debug(sprintf('Going to store a %s.', $type->type)); + if (0 === \count($transactions)) { + Log::error('There are no transactions in the array, cannot continue.'); + + return new Collection; + } + + /** @var array $row */ + foreach ($transactions as $index => $row) { + $transaction = new NullArrayObject($row); + Log::debug(sprintf('Now creating journal %d/%d', $index + 1, \count($transactions))); /** Get basic fields */ - $currency = $this->currencyRepository->findCurrency($transaction['currency'], $transaction['currency_id'], $transaction['currency_code']); + + $currency = $this->currencyRepository->findCurrency($transaction['currency'], $transaction['currency_id'], $transaction['currency_code']); + $foreignCurrency = $this->findForeignCurrency($transaction); + + $bill = $this->billRepository->findBill($transaction['bill'], $transaction['bill_id'], $transaction['bill_name']); + $billId = TransactionType::WITHDRAWAL === $type->type && null !== $bill ? $bill->id : null; + $description = app('steam')->cleanString($transaction['description']); /** Create a basic journal. */ $journal = TransactionJournal::create( [ 'user_id' => $this->user->id, 'transaction_type_id' => $type->id, - 'bill_id' => null, + 'bill_id' => $billId, 'transaction_currency_id' => $currency->id, 'description' => $description, 'date' => $carbon->format('Y-m-d H:i:s'), @@ -108,131 +143,42 @@ class TransactionJournalFactory 'completed' => 0, ] ); + Log::debug(sprintf('Created new journal #%d: "%s"', $journal->id, $journal->description)); /** Create two transactions. */ $this->transactionFactory->setJournal($journal); - $children = $this->transactionFactory->createPair($currency, $transaction); + $this->transactionFactory->createPair($transaction, $currency, $foreignCurrency); + /** Link all other data to the journal. */ + /** Link budget */ + $this->storeBudget($journal, $transaction); + /** Link category */ + $this->storeCategory($journal, $transaction); + + /** Set notes */ + $this->storeNote($journal, $transaction['notes']); + + /** Set piggy bank */ + $this->storePiggyEvent($journal, $transaction); + + /** Set tags */ + $this->storeTags($journal, $transaction['tags']); + + /** set all meta fields */ + $this->storeMetaFields($journal, $transaction); $collection->push($journal); - Log::debug(sprintf('Created journal #%d', $journal->id)); - - return $collection; - - - /** Create two basic transactions */ + } + if ($collection->count() > 1) { + $this->storeGroup($collection, $data['group_title']); } + return $collection; - // /** @var TransactionFactory $factory */ - // $factory = app(TransactionFactory::class); - // $journals = new Collection; - // ; - // $type = $this->findTransactionType($data['type']); - // - // - // - // $factory->setUser($this->user); - // - // Log::debug(sprintf('New journal(group): %s with description "%s"', $type->type, $description)); - // - // // loop each transaction. - // /** - // * @var int $index - // * @var array $transactionData - // */ - // foreach ($data['transactions'] as $index => $transactionData) { - // Log::debug(sprintf('Now at journal #%d from %d', $index + 1, count($data['transactions']))); - // - // // catch to stop empty amounts: - // if ('' === (string)$transactionData['amount'] || 0.0 === (float)$transactionData['amount']) { - // continue; - // } - // // currency & foreign currency - // $transactionData['currency'] = $this->getCurrency($transactionData, $index); - // $transactionData['foreign_currency'] = $this->getForeignCurrency($transactionData, $index); - // - // // store basic journal first. - // $journal = TransactionJournal::create( - // [ - // 'user_id' => $data['user'], - // 'transaction_type_id' => $type->id, - // 'bill_id' => null, - // 'transaction_currency_id' => $transactionData['currency']->id, - // 'description' => $description, - // 'date' => $carbon->format('Y-m-d H:i:s'), - // 'order' => 0, - // 'tag_count' => 0, - // 'completed' => 0, - // ] - // ); - // Log::debug(sprintf('Stored journal under ID #%d', $journal->id)); - // - // // store transactions for this journal: - // $factory->createPair($journal, $transactionData); - // - // // link bill - // Log::debug('Connect bill'); - // $this->connectBill($journal, $transactionData); - // - // // link piggy bank (if transfer) - // $this->connectPiggyBank($journal, $transactionData); - // - // // link tags - // $this->connectTags($journal, $transactionData); - // - // // store note - // $this->storeNote($journal, $transactionData['notes']); - // - // // save journal: - // $journal->completed = true; - // $journal->save(); - // - // - // - // // if ($journal->transactionType->type !== TransactionType::WITHDRAWAL) { - // // $transactionData['budget_id'] = null; - // // $transactionData['budget_name'] = null; - // // } - // // // save budget TODO - // // $budget = $this->findBudget($data['budget_id'], $data['budget_name']); - // // $this->setBudget($journal, $budget); - // // - // // // set category TODO - // // $category = $this->findCategory($data['category_id'], $data['category_name']); - // // $this->setCategory($journal, $category); - // // - // // // store meta data TODO - // // foreach ($this->fields as $field) { - // // $this->storeMeta($journal, $data, $field); - // // } - // - // // add to array - // $journals->push($journal); - // } - // - // // create group if necessary - // if ($journals->count() > 1) { - // $group = new TransactionGroup; - // $group->user()->associate($this->user); - // $group->title = $description; - // $group->save(); - // $group->transactionJournals()->saveMany($journals); - // - // Log::debug(sprintf('More than one journal, created group #%d.', $group->id)); - // } - // - // - // Log::debug('End of TransactionJournalFactory::create()'); - // // invalidate cache. - // app('preferences')->mark(); - // - // return $journals; } - /** * Set the user. * @@ -243,165 +189,214 @@ class TransactionJournalFactory $this->user = $user; $this->currencyRepository->setUser($this->user); $this->transactionFactory->setUser($this->user); + $this->billRepository->setUser($this->user); + $this->budgetRepository->setUser($this->user); + $this->categoryRepository->setUser($this->user); + $this->piggyRepository->setUser($this->user); + } + + /** + * Join multiple journals in a group. + * + * @param Collection $collection + * @param string|null $title + * + * @return TransactionGroup|null + */ + public function storeGroup(Collection $collection, ?string $title): ?TransactionGroup + { + if ($collection->count() < 2) { + return null; + } + /** @var TransactionJournal $first */ + $first = $collection->first(); + $group = new TransactionGroup; + $group->user()->associate($first->user); + $group->title = $title ?? $first->description; + $group->save(); + + $group->transactionJournals()->saveMany($collection); + + return $group; + } + + /** + * Link a piggy bank to this journal. + * + * @param TransactionJournal $journal + * @param NullArrayObject $data + */ + public function storePiggyEvent(TransactionJournal $journal, NullArrayObject $data): void + { + Log::debug('Will now store piggy event.'); + if (!$journal->isTransfer()) { + Log::debug('Journal is not a transfer, do nothing.'); + + return; + } + + $piggyBank = $this->piggyRepository->findPiggyBank($data['piggy_bank'], $data['piggy_bank_id'], $data['piggy_bank_name']); + + if (null !== $piggyBank) { + /** @var PiggyBankEventFactory $factory */ + $factory = app(PiggyBankEventFactory::class); + $factory->create($journal, $piggyBank); + Log::debug('Create piggy event.'); + } + + + /** @var PiggyBankFactory $factory */ + $factory = app(PiggyBankFactory::class); + $factory->setUser($this->user); + $piggyBank = null; + + if (isset($data['piggy_bank']) && $data['piggy_bank'] instanceof PiggyBank && $data['piggy_bank']->account->user_id === $this->user->id) { + Log::debug('Piggy found and belongs to user'); + $piggyBank = $data['piggy_bank']; + } + if (null === $data['piggy_bank']) { + Log::debug('Piggy not found, search by piggy data.'); + $piggyBank = $factory->find($data['piggy_bank_id'], $data['piggy_bank_name']); + } + + if (null !== $piggyBank) { + + + return; + } + Log::debug('Create no piggy event'); + } + + /** + * Link tags to journal. + * + * @param TransactionJournal $journal + * @param array $tags + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function storeTags(TransactionJournal $journal, ?array $tags): void + { + /** @var TagFactory $factory */ + $factory = app(TagFactory::class); + $factory->setUser($journal->user); + $set = []; + if (!\is_array($tags)) { + return; // @codeCoverageIgnore + } + foreach ($tags as $string) { + if ('' !== $string) { + $tag = $factory->findOrCreate($string); + if (null !== $tag) { + $set[] = $tag->id; + } + } + } + $journal->tags()->sync($set); + } + + /** + * @param TransactionJournal $journal + * @param NullArrayObject $data + * @param string $field + */ + protected function storeMeta(TransactionJournal $journal, NullArrayObject $data, string $field): void + { + $set = [ + 'journal' => $journal, + 'name' => $field, + 'data' => (string)($data[$field] ?? ''), + ]; + + Log::debug(sprintf('Going to store meta-field "%s", with value "%s".', $set['name'], $set['data'])); + + /** @var TransactionJournalMetaFactory $factory */ + $factory = app(TransactionJournalMetaFactory::class); + $factory->updateOrCreate($set); + } + + /** + * @param TransactionJournal $journal + * @param string $notes + */ + protected function storeNote(TransactionJournal $journal, ?string $notes): void + { + $notes = (string)$notes; + if ('' !== $notes) { + $note = $journal->notes()->first(); + if (null === $note) { + $note = new Note; + $note->noteable()->associate($journal); + } + $note->text = $notes; + $note->save(); + Log::debug(sprintf('Stored notes for journal #%d', $journal->id)); + + return; + } + $note = $journal->notes()->first(); + if (null !== $note) { + try { + $note->delete(); + } catch (Exception $e) { + Log::debug(sprintf('Journal service trait could not delete note: %s', $e->getMessage())); + } + } + } + + /** + * This is a separate function because "findCurrency" will default to EUR and that may not be what we want. + * + * @param NullArrayObject $transaction + * + * @return TransactionCurrency|null + */ + private function findForeignCurrency(NullArrayObject $transaction): ?TransactionCurrency + { + if (null === $transaction['foreign_currency'] && null === $transaction['foreign_currency_id'] && null === $transaction['foreign_currency_code']) { + return null; + } + + return $this->currencyRepository->findCurrency( + $transaction['foreign_currency'], $transaction['foreign_currency_id'], $transaction['foreign_currency_code'] + ); + } + + /** + * @param TransactionJournal $journal + * @param NullArrayObject $data + */ + private function storeBudget(TransactionJournal $journal, NullArrayObject $data): void + { + $budget = $this->budgetRepository->findBudget($data['budget'], $data['budget_id'], $data['budget_name']); + if (null !== $budget) { + Log::debug(sprintf('Link budget #%d to journal #%d', $budget->id, $journal->id)); + $journal->budgets()->sync([$budget->id]); + } + } + + /** + * @param TransactionJournal $journal + * @param NullArrayObject $data + */ + private function storeCategory(TransactionJournal $journal, NullArrayObject $data): void + { + $category = $this->categoryRepository->findCategory($data['category'], $data['category_id'], $data['category_name']); + if (null !== $category) { + Log::debug(sprintf('Link category #%d to journal #%d', $category->id, $journal->id)); + $journal->categories()->sync([$category->id]); + } + } + + /** + * @param TransactionJournal $journal + * @param NullArrayObject $transaction + */ + private function storeMetaFields(TransactionJournal $journal, NullArrayObject $transaction): void + { + foreach ($this->fields as $field) { + $this->storeMeta($journal, $transaction, $field); + } } - // /** - // * Connect bill if present. - // * - // * @param TransactionJournal $journal - // * @param array $data - // */ - // protected function connectBill(TransactionJournal $journal, array $data): void - // { - // if (!$journal->isWithdrawal()) { - // Log::debug(sprintf('Journal #%d is not a withdrawal', $journal->id)); - // - // return; - // } - // /** @var BillFactory $factory */ - // $factory = app(BillFactory::class); - // $factory->setUser($journal->user); - // - // $bill = null; - // - // if (isset($data['bill']) && $data['bill'] instanceof Bill && $data['bill']->user_id === $this->user->id) { - // Log::debug('Bill object found and belongs to user'); - // $bill = $data['bill']; - // } - // if (null === $data['bill']) { - // Log::debug('Bill object not found, search by bill data.'); - // $bill = $factory->find((int)$data['bill_id'], $data['bill_name']); - // } - // - // if (null !== $bill) { - // Log::debug(sprintf('Connected bill #%d (%s) to journal #%d', $bill->id, $bill->name, $journal->id)); - // $journal->bill_id = $bill->id; - // $journal->save(); - // - // return; - // } - // Log::debug('Bill data is NULL.'); - // $journal->bill_id = null; - // $journal->save(); - // - // } - // - // /** - // * Link a piggy bank to this journal. - // * - // * @param TransactionJournal $journal - // * @param array $data - // */ - // protected function connectPiggyBank(TransactionJournal $journal, array $data): void - // { - // if (!$journal->isTransfer()) { - // - // return; - // } - // /** @var PiggyBankFactory $factory */ - // $factory = app(PiggyBankFactory::class); - // $factory->setUser($this->user); - // $piggyBank = null; - // - // if (isset($data['piggy_bank']) && $data['piggy_bank'] instanceof PiggyBank && $data['piggy_bank']->account->user_id === $this->user->id) { - // Log::debug('Piggy found and belongs to user'); - // $piggyBank = $data['piggy_bank']; - // } - // if (null === $data['piggy_bank']) { - // Log::debug('Piggy not found, search by piggy data.'); - // $piggyBank = $factory->find($data['piggy_bank_id'], $data['piggy_bank_name']); - // } - // - // if (null !== $piggyBank) { - // /** @var PiggyBankEventFactory $factory */ - // $factory = app(PiggyBankEventFactory::class); - // $factory->create($journal, $piggyBank); - // Log::debug('Create piggy event.'); - // - // return; - // } - // Log::debug('Create no piggy event'); - // } - // - // /** - // * @param array $data - // * @param int $index - // * - // * @return TransactionCurrency - // */ - // private function getCurrency(array $data, int $index): TransactionCurrency - // { - // $currency = null; - // // check currency object: - // if (null === $currency && isset($data['currency']) && $data['currency'] instanceof TransactionCurrency) { - // $currency = $data['currency']; - // } - // - // // check currency ID: - // if (null === $currency && isset($data['currency_id']) && (int)$data['currency_id'] > 0) { - // $currencyId = (int)$data['currency_id']; - // $currency = TransactionCurrency::find($currencyId); - // } - // - // // check currency code - // if (null === $currency && isset($data['currency_code']) && 3 === \strlen($data['currency_code'])) { - // $currency = TransactionCurrency::whereCode($data['currency_code'])->first(); - // } - // if (null === $currency) { - // // return user's default currency: - // $currency = app('amount')->getDefaultCurrencyByUser($this->user); - // } - // - // // enable currency: - // if (false === $currency->enabled) { - // $currency->enabled = true; - // $currency->save(); - // } - // Log::debug(sprintf('Journal currency will be #%d (%s)', $currency->id, $currency->code)); - // - // return $currency; - // - // } - // - // /** - // * @param array $data - // * @param int $index - // * - // * @return TransactionCurrency|null - // */ - // private function getForeignCurrency(array $data, int $index): ?TransactionCurrency - // { - // $currency = null; - // - // // check currency object: - // if (null === $currency && isset($data['foreign_currency']) && $data['foreign_currency'] instanceof TransactionCurrency) { - // $currency = $data['foreign_currency']; - // } - // - // // check currency ID: - // if (null === $currency && isset($data['foreign_currency_id']) && (int)$data['foreign_currency_id'] > 0) { - // $currencyId = (int)$data['foreign_currency_id']; - // $currency = TransactionCurrency::find($currencyId); - // } - // - // // check currency code - // if (null === $currency && isset($data['foreign_currency_code']) && 3 === \strlen($data['foreign_currency_code'])) { - // $currency = TransactionCurrency::whereCode($data['foreign_currency_code'])->first(); - // } - // - // // enable currency: - // if (null !== $currency && false === $currency->enabled) { - // $currency->enabled = true; - // $currency->save(); - // } - // if (null !== $currency) { - // Log::debug(sprintf('Journal foreign currency will be #%d (%s)', $currency->id, $currency->code)); - // } - // if (null === $currency) { - // Log::debug('Journal foreign currency will be NULL'); - // } - // - // return $currency; - // } } diff --git a/app/Repositories/Bill/BillRepository.php b/app/Repositories/Bill/BillRepository.php index f7888eb4ce..6167e25227 100644 --- a/app/Repositories/Bill/BillRepository.php +++ b/app/Repositories/Bill/BillRepository.php @@ -86,6 +86,45 @@ class BillRepository implements BillRepositoryInterface return $this->user->bills()->find($billId); } + /** + * Find bill by parameters. + * + * @param Bill|null $bill + * @param int|null $billId + * @param string|null $billName + * + * @return Bill|null + */ + public function findBill(?Bill $bill, ?int $billId, ?string $billName): ?Bill + { + Log::debug('Searching for bill information.'); + if ($bill instanceof Bill && $bill->user_id === $this->user->id) { + Log::debug(sprintf('Bill object in parameters, will return Bill #%d', $bill->id)); + + return $bill; + } + + if (null !== $billId) { + $searchResult = $this->find((int)$billId); + if (null !== $searchResult) { + Log::debug(sprintf('Found bill based on #%d, will return it.', $billId)); + + return $searchResult; + } + } + if (null !== $billName) { + $searchResult = $this->findByName((string)$billName); + if (null !== $searchResult) { + Log::debug(sprintf('Found bill based on "%s", will return it.', $billName)); + + return $searchResult; + } + } + Log::debug('Found nothing'); + + return null; + } + /** * Find a bill by name. * diff --git a/app/Repositories/Bill/BillRepositoryInterface.php b/app/Repositories/Bill/BillRepositoryInterface.php index e90abc44e8..c883465fc3 100644 --- a/app/Repositories/Bill/BillRepositoryInterface.php +++ b/app/Repositories/Bill/BillRepositoryInterface.php @@ -49,6 +49,17 @@ interface BillRepositoryInterface */ public function find(int $billId): ?Bill; + /** + * Find bill by parameters. + * + * @param Bill|null $bill + * @param int|null $billId + * @param string|null $billName + * + * @return Bill|null + */ + public function findBill(?Bill $bill, ?int $billId, ?string $billName): ?Bill; + /** * Find a bill by name. * diff --git a/app/Repositories/Budget/BudgetRepository.php b/app/Repositories/Budget/BudgetRepository.php index e079b4edaa..8ee5746134 100644 --- a/app/Repositories/Budget/BudgetRepository.php +++ b/app/Repositories/Budget/BudgetRepository.php @@ -218,6 +218,55 @@ class BudgetRepository implements BudgetRepositoryInterface } } + /** + * @param Budget|null $budget + * @param int|null $budgetId + * @param string|null $budgetName + * + * @return Budget|null + */ + public function findBudget(?Budget $budget, ?int $budgetId, ?string $budgetName): ?Budget + { + Log::debug('Now in findBudget()'); + $result = null; + if (null !== $budget) { + Log::debug(sprintf('Parameters contain budget #%d, will return this.', $budget->id)); + $result = $budget; + } + + if (null === $result) { + Log::debug(sprintf('Searching for budget with ID #%d...', $budgetId)); + $result = $this->findNull((int)$budgetId); + } + if (null === $result) { + Log::debug(sprintf('Searching for budget with name %s...', $budgetName)); + $result = $this->findByName((string)$budgetName); + } + if (null !== $result) { + Log::debug(sprintf('Found budget #%d: %s', $result->id, $result->name)); + } + Log::debug(sprintf('Found result is null? %s', var_export(null === $result, true))); + + return $result; + } + + /** + * Find budget by name. + * + * @param string|null $name + * + * @return Budget|null + */ + public function findByName(?string $name): ?Budget + { + if (null === $name) { + return null; + } + $query = sprintf('%%%s%%', $name); + + return $this->user->budgets()->where('name', 'LIKE', $query)->first(); + } + /** * Find a budget or return NULL * @@ -399,6 +448,8 @@ class BudgetRepository implements BudgetRepositoryInterface return $return; } + /** @noinspection MoreThanThreeArgumentsInspection */ + /** * Returns all available budget objects. * @@ -439,8 +490,6 @@ class BudgetRepository implements BudgetRepositoryInterface return bcdiv($total, (string)$days); } - /** @noinspection MoreThanThreeArgumentsInspection */ - /** * @param Budget $budget * @param Carbon $start @@ -568,6 +617,8 @@ class BudgetRepository implements BudgetRepositoryInterface return $set; } + /** @noinspection MoreThanThreeArgumentsInspection */ + /** * Get all budgets with these ID's. * @@ -647,8 +698,6 @@ class BudgetRepository implements BudgetRepositoryInterface return $this->user->budgets()->where('name', 'LIKE', $query)->get(); } - /** @noinspection MoreThanThreeArgumentsInspection */ - /** * @param TransactionCurrency $currency * @param Carbon $start @@ -853,6 +902,8 @@ class BudgetRepository implements BudgetRepositoryInterface return $return; } + /** @noinspection MoreThanThreeArgumentsInspection */ + /** * @param array $data * @@ -915,8 +966,6 @@ class BudgetRepository implements BudgetRepositoryInterface return $limit; } - /** @noinspection MoreThanThreeArgumentsInspection */ - /** * @param Budget $budget * @param array $data diff --git a/app/Repositories/Budget/BudgetRepositoryInterface.php b/app/Repositories/Budget/BudgetRepositoryInterface.php index d61c5ed5ee..88c729cd19 100644 --- a/app/Repositories/Budget/BudgetRepositoryInterface.php +++ b/app/Repositories/Budget/BudgetRepositoryInterface.php @@ -35,6 +35,7 @@ use Illuminate\Support\Collection; */ interface BudgetRepositoryInterface { + /** * A method that returns the amount of money budgeted per day for this budget, * on average. @@ -80,6 +81,24 @@ interface BudgetRepositoryInterface */ public function destroyBudgetLimit(BudgetLimit $budgetLimit): void; + /** + * @param Budget|null $budget + * @param int|null $budgetId + * @param string|null $budgetName + * + * @return Budget|null + */ + public function findBudget(?Budget $budget, ?int $budgetId, ?string $budgetName): ?Budget; + + /** + * Find budget by name. + * + * @param string|null $name + * + * @return Budget|null + */ + public function findByName(?string $name): ?Budget; + /** * @param int|null $budgetId * diff --git a/app/Repositories/Category/CategoryRepository.php b/app/Repositories/Category/CategoryRepository.php index 24f695623b..0768781dd6 100644 --- a/app/Repositories/Category/CategoryRepository.php +++ b/app/Repositories/Category/CategoryRepository.php @@ -252,6 +252,42 @@ class CategoryRepository implements CategoryRepositoryInterface return null; } + /** + * @param Category|null $category + * @param int|null $categoryId + * @param string|null $categoryName + * + * @return Category|null + */ + public function findCategory(?Category $category, ?int $categoryId, ?string $categoryName): ?Category + { + Log::debug('Now in findCategory()'); + $result = null; + if (null !== $category) { + Log::debug(sprintf('Parameters contain category #%d, will return this.', $category->id)); + $result = $category; + } + + if (null === $result) { + Log::debug(sprintf('Searching for category with ID #%d...', $categoryId)); + $result = $this->findNull((int)$categoryId); + } + if (null === $result) { + Log::debug(sprintf('Searching for category with name %s...', $categoryName)); + $result = $this->findByName((string)$categoryName); + if (null === $result && '' !== (string)$categoryName) { + // create it! + $result = $this->store(['name' => $categoryName]); + } + } + if (null !== $result) { + Log::debug(sprintf('Found category #%d: %s', $result->id, $result->name)); + } + Log::debug(sprintf('Found category result is null? %s', var_export(null === $result, true))); + + return $result; + } + /** * Find a category or return NULL * @@ -264,6 +300,8 @@ class CategoryRepository implements CategoryRepositoryInterface return $this->user->categories()->find($categoryId); } + /** @noinspection MoreThanThreeArgumentsInspection */ + /** * @param Category $category * @@ -292,8 +330,6 @@ class CategoryRepository implements CategoryRepositoryInterface return $firstJournalDate; } - /** @noinspection MoreThanThreeArgumentsInspection */ - /** * Get all categories with ID's. * @@ -306,6 +342,8 @@ class CategoryRepository implements CategoryRepositoryInterface return $this->user->categories()->whereIn('id', $categoryIds)->get(); } + /** @noinspection MoreThanThreeArgumentsInspection */ + /** * Returns a list of all the categories belonging to a user. * @@ -324,8 +362,6 @@ class CategoryRepository implements CategoryRepositoryInterface return $set; } - /** @noinspection MoreThanThreeArgumentsInspection */ - /** * @param Category $category * @param Collection $accounts @@ -401,6 +437,8 @@ class CategoryRepository implements CategoryRepositoryInterface return $data; } + /** @noinspection MoreThanThreeArgumentsInspection */ + /** * @param Collection $accounts * @param Carbon $start @@ -529,8 +567,6 @@ class CategoryRepository implements CategoryRepositoryInterface return $result; } - /** @noinspection MoreThanThreeArgumentsInspection */ - /** * @param string $query * @@ -839,6 +875,7 @@ class CategoryRepository implements CategoryRepositoryInterface * @param Collection $accounts * * @return Carbon|null + * @throws \Exception */ private function getLastTransactionDate(Category $category, Collection $accounts): ?Carbon { diff --git a/app/Repositories/Category/CategoryRepositoryInterface.php b/app/Repositories/Category/CategoryRepositoryInterface.php index edd4a33597..e6d4730d89 100644 --- a/app/Repositories/Category/CategoryRepositoryInterface.php +++ b/app/Repositories/Category/CategoryRepositoryInterface.php @@ -33,6 +33,15 @@ use Illuminate\Support\Collection; interface CategoryRepositoryInterface { + /** + * @param Category|null $category + * @param int|null $categoryId + * @param string|null $categoryName + * + * @return Category|null + */ + public function findCategory(?Category $category, ?int $categoryId, ?string $categoryName): ?Category; + /** * @param Category $category * diff --git a/app/Repositories/Currency/CurrencyRepository.php b/app/Repositories/Currency/CurrencyRepository.php index 306a05a5e9..17653f258d 100644 --- a/app/Repositories/Currency/CurrencyRepository.php +++ b/app/Repositories/Currency/CurrencyRepository.php @@ -266,24 +266,32 @@ class CurrencyRepository implements CurrencyRepositoryInterface */ public function findCurrency(?TransactionCurrency $currency, ?int $currencyId, ?string $currencyCode): TransactionCurrency { + Log::debug('Now in findCurrency()'); $result = null; if (null !== $currency) { + Log::debug(sprintf('Parameters contain %s, will return this.', $currency->code)); $result = $currency; } if (null === $result) { + Log::debug(sprintf('Searching for currency with ID #%d...', $currencyId)); $result = $this->find((int)$currencyId); } if (null === $result) { + Log::debug(sprintf('Searching for currency with code %s...', $currencyCode)); $result = $this->findByCode((string)$currencyCode); } if (null === $result) { + Log::debug('Grabbing default currency for this user...'); $result = app('amount')->getDefaultCurrencyByUser($this->user); } if (null === $result) { + Log::debug('Grabbing EUR as fallback.'); $result = $this->findByCode('EUR'); } + Log::debug(sprintf('Final result: %s', $result->code)); if (false === $result->enabled) { + Log::debug(sprintf('Also enabled currency %s', $result->code)); $this->enable($result); } diff --git a/app/Repositories/PiggyBank/PiggyBankRepository.php b/app/Repositories/PiggyBank/PiggyBankRepository.php index dda694106c..f6a6e56df1 100644 --- a/app/Repositories/PiggyBank/PiggyBankRepository.php +++ b/app/Repositories/PiggyBank/PiggyBankRepository.php @@ -224,6 +224,43 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface return null; } + /** + * @param PiggyBank|null $piggyBank + * @param int|null $piggyBankId + * @param string|null $piggyBankName + * + * @return PiggyBank|null + */ + public function findPiggyBank(?PiggyBank $piggyBank, ?int $piggyBankId, ?string $piggyBankName): ?PiggyBank + { + Log::debug('Searching for piggy information.'); + if ($piggyBank instanceof PiggyBank && $piggyBank->account->user_id === $this->user->id) { + Log::debug(sprintf('Piggy object in parameters, will return Piggy #%d', $piggyBank->id)); + + return $piggyBank; + } + + if (null !== $piggyBankId) { + $searchResult = $this->findNull((int)$piggyBankId); + if (null !== $searchResult) { + Log::debug(sprintf('Found piggy based on #%d, will return it.', $piggyBankId)); + + return $searchResult; + } + } + if (null !== $piggyBankName) { + $searchResult = $this->findByName((string)$piggyBankName); + if (null !== $searchResult) { + Log::debug(sprintf('Found piggy based on "%s", will return it.', $piggyBankName)); + + return $searchResult; + } + } + Log::debug('Found nothing'); + + return null; + } + /** * Get current amount saved in piggy bank. * diff --git a/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php b/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php index c8a789dc68..997137e967 100644 --- a/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php +++ b/app/Repositories/PiggyBank/PiggyBankRepositoryInterface.php @@ -35,7 +35,6 @@ use Illuminate\Support\Collection; */ interface PiggyBankRepositoryInterface { - /** * @param PiggyBank $piggyBank * @param string $amount @@ -117,6 +116,15 @@ interface PiggyBankRepositoryInterface */ public function findNull(int $piggyBankId): ?PiggyBank; + /** + * @param PiggyBank|null $piggyBank + * @param int|null $piggyBankId + * @param string|null $piggyBankName + * + * @return PiggyBank|null + */ + public function findPiggyBank(?PiggyBank $piggyBank, ?int $piggyBankId, ?string $piggyBankName): ?PiggyBank; + /** * Get current amount saved in piggy bank. * diff --git a/app/Repositories/TransactionType/TransactionTypeRepository.php b/app/Repositories/TransactionType/TransactionTypeRepository.php index 899577b846..41f64d6854 100644 --- a/app/Repositories/TransactionType/TransactionTypeRepository.php +++ b/app/Repositories/TransactionType/TransactionTypeRepository.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Repositories\TransactionType; use FireflyIII\Models\TransactionType; - +use Log; /** * Class TransactionTypeRepository */ @@ -51,13 +51,17 @@ class TransactionTypeRepository implements TransactionTypeRepositoryInterface */ public function findTransactionType(?TransactionType $type, ?string $typeString): TransactionType { + Log::debug('Now looking for a transaction type.'); if (null !== $type) { + Log::debug(sprintf('Found $type in parameters, its %s. Will return it.', $type->type)); + return $type; } $search = $this->findByType($typeString); if (null === $search) { $search = $this->findByType(TransactionType::WITHDRAWAL); } + Log::debug(sprintf('Tried to search for "%s", came up with "%s". Will return it.', $typeString, $search->type)); return $search; } diff --git a/app/Support/NullArrayObject.php b/app/Support/NullArrayObject.php new file mode 100644 index 0000000000..5f7728a86f --- /dev/null +++ b/app/Support/NullArrayObject.php @@ -0,0 +1,58 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Support; + + +use ArrayObject; + +class NullArrayObject extends ArrayObject +{ + public $default = null; + + /** + * NullArrayObject constructor. + * + * @param array $array + * @param null $default + */ + public function __construct(array $array, $default = null) + { + parent::__construct($array); + $this->default = $default; + } + + /** + * @param mixed $key + * + * @return mixed|null + */ + public function offsetGet($key) + { + if ($this->offsetExists($key)) { + return parent::offsetGet($key); + } + + return null; + } +} \ No newline at end of file diff --git a/resources/views/v1/accounts/reconcile/transactions.twig b/resources/views/v1/accounts/reconcile/transactions.twig index 8a9e4b51fe..3ab2ffdf41 100644 --- a/resources/views/v1/accounts/reconcile/transactions.twig +++ b/resources/views/v1/accounts/reconcile/transactions.twig @@ -88,7 +88,7 @@ {% set transactionAmount = transaction.transaction_foreign_amount %} {% endif %} - {% if transaction.reconciled %} + {% if transaction.$array[$direction][$transactionType] %} {{ transaction|transactionReconciled }} {# is reconciled? #} - {{ transaction|transactionReconciled }} + {{ transaction|transaction$array[$direction][$transactionType] }} {{ transaction|transactionDescription }} diff --git a/resources/views/v1/transactions/show.twig b/resources/views/v1/transactions/show.twig index 5b254c43a7..611df36916 100644 --- a/resources/views/v1/transactions/show.twig +++ b/resources/views/v1/transactions/show.twig @@ -10,7 +10,6 @@

{{ 'transaction_journal_information'|_ }}

-