. */ namespace FireflyIII\Console\Commands\Upgrade; use Exception; use FireflyIII\Factory\TransactionJournalFactory; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionGroup; use FireflyIII\Models\TransactionJournal; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use FireflyIII\Services\Internal\Destroy\JournalDestroyService; use Illuminate\Console\Command; 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 { public const CONFIG_NAME = '4780_migrated_to_groups'; /** * 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-iii:migrate-to-groups {--F|force : Force the migration, even if it fired before.}'; /** @var TransactionJournalFactory */ private $journalFactory; /** @var JournalRepositoryInterface */ private $journalRepository; /** @var JournalDestroyService */ private $service; /** * Create a new command instance. * * @return void */ public function __construct() { parent::__construct(); $this->journalFactory = app(TransactionJournalFactory::class); $this->journalRepository = app(JournalRepositoryInterface::class); $this->service = app(JournalDestroyService::class); } /** * 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.'); return 0; } if (true === $this->option('force')) { $this->warn('Forcing the migration.'); } Log::debug('---- start group migration ----'); $this->makeGroupsFromSplitJournals(); $this->makeGroupsFromAll(); Log::debug('---- end group migration ----'); $this->markAsMigrated(); return 0; } /** * @param TransactionJournal $journal */ private function giveGroup(TransactionJournal $journal): void { $group = new TransactionGroup; $group->title = null; $group->user_id = $journal->user_id; $group->save(); $journal->transaction_group_id = $group->id; $journal->save(); } /** * @return bool */ private function isMigrated(): bool { $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false); if (null !== $configVar) { return (bool)$configVar->data; } return false; // @codeCoverageIgnore } /** * Gives all journals without a group a group. */ private function makeGroupsFromAll(): void { $orphanedJournals = $this->journalRepository->getJournalsWithoutGroup(); if ($orphanedJournals->count() > 0) { Log::debug(sprintf('Going to convert %d transactions. Please hold..', $orphanedJournals->count())); /** @var TransactionJournal $journal */ foreach ($orphanedJournals as $journal) { $this->giveGroup($journal); } } if (0 === $orphanedJournals->count()) { $this->info('No need to convert transactions.'); } } /** * * @throws Exception */ private function makeGroupsFromSplitJournals(): void { $splitJournals = $this->journalRepository->getSplitJournals(); if ($splitJournals->count() > 0) { $this->info(sprintf('Going to convert %d split transaction(s). Please hold..', $splitJournals->count())); /** @var TransactionJournal $journal */ foreach ($splitJournals as $journal) { $this->makeMultiGroup($journal); } } if (0 === $splitJournals->count()) { $this->info('Found no split transactions. Nothing to do.'); } } /** * @param TransactionJournal $journal * * @throws Exception */ private function makeMultiGroup(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); $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->journalRepository->getJournalBudgetId($journal); $categoryId = $this->journalRepository->getJournalCategoryId($journal); $opposingTr = $this->journalRepository->findOpposingTransaction($transaction); if (null === $opposingTr) { $this->error( sprintf( 'Journal #%d has no opposing transaction for transaction #%d. Cannot upgrade this entry.', $journal->id, $transaction->id ) ); continue; } $tArray = [ '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' => $this->journalRepository->getTags($journal), '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. $this->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()))); } /** * */ private function markAsMigrated(): void { app('fireflyconfig')->set(self::CONFIG_NAME, true); } }