GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Passed
Push — master ( d550a6...996863 )
by James
24:09 queued 11:56
created

MigrateToGroups::getTransactionBudget()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 25
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 11
c 1
b 0
f 0
dl 0
loc 25
rs 9.9
cc 3
nc 3
nop 2
1
<?php
2
declare(strict_types=1);
3
/**
4
 * MigrateToGroups.php
5
 * Copyright (c) 2019 [email protected]
6
 *
7
 * This file is part of Firefly III.
8
 *
9
 * Firefly III is free software: you can redistribute it and/or modify
10
 * it under the terms of the GNU General Public License as published by
11
 * the Free Software Foundation, either version 3 of the License, or
12
 * (at your option) any later version.
13
 *
14
 * Firefly III is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
 * GNU General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU General Public License
20
 * along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
21
 */
22
23
namespace FireflyIII\Console\Commands\Upgrade;
24
25
use DB;
26
use Exception;
27
use FireflyIII\Factory\TransactionGroupFactory;
28
use FireflyIII\Models\Budget;
29
use FireflyIII\Models\Category;
30
use FireflyIII\Models\Transaction;
31
use FireflyIII\Models\TransactionJournal;
32
use FireflyIII\Repositories\Journal\JournalCLIRepositoryInterface;
33
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
34
use FireflyIII\Services\Internal\Destroy\JournalDestroyService;
35
use Illuminate\Console\Command;
36
use Illuminate\Support\Collection;
37
use Log;
38
39
/**
40
 * This command will take split transactions and migrate them to "transaction groups".
41
 *
42
 * It will only run once, but can be forced to run again.
43
 *
44
 * Class MigrateToGroups
45
 */
46
class MigrateToGroups extends Command
47
{
48
    public const CONFIG_NAME = '480_migrated_to_groups';
49
    /**
50
     * The console command description.
51
     *
52
     * @var string
53
     */
54
    protected $description = 'Migrates a pre-4.7.8 transaction structure to the 4.7.8+ transaction structure.';
55
    /**
56
     * The name and signature of the console command.
57
     *
58
     * @var string
59
     */
60
    protected $signature = 'firefly-iii:migrate-to-groups {--F|force : Force the migration, even if it fired before.}';
61
    /** @var TransactionGroupFactory */
62
    private $groupFactory;
63
    /** @var JournalRepositoryInterface */
64
    private $journalRepository;
65
    /** @var JournalCLIRepositoryInterface */
66
    private $cliRepository;
67
    /** @var JournalDestroyService */
68
    private $service;
69
    private $count;
70
71
    /**
72
     * Execute the console command.
73
     *
74
     * @return int
75
     * @throws Exception
76
     */
77
    public function handle(): int
78
    {
79
        $this->stupidLaravel();
80
        $start = microtime(true);
81
        // @codeCoverageIgnoreStart
82
        if ($this->isMigrated() && true !== $this->option('force')) {
83
            $this->info('Database already seems to be migrated.');
84
85
            return 0;
86
        }
87
88
        if (true === $this->option('force')) {
0 ignored issues
show
introduced by
The condition true === $this->option('force') is always false.
Loading history...
89
            $this->warn('Forcing the migration.');
90
        }
91
        // @codeCoverageIgnoreEnd
92
93
        Log::debug('---- start group migration ----');
94
        $this->makeGroupsFromSplitJournals();
95
        $end = round(microtime(true) - $start, 2);
96
        $this->info(sprintf('Migrate split journals to groups in %s seconds.', $end));
97
98
        $start = microtime(true);
99
        $this->makeGroupsFromAll();
100
        Log::debug('---- end group migration ----');
101
        $end = round(microtime(true) - $start, 2);
102
        $this->info(sprintf('Migrate all journals to groups in %s seconds.', $end));
103
104
        if (0 !== $this->count) {
105
            $this->line(sprintf('Migrated %d transaction journal(s).', $this->count));
106
        }
107
        if (0 === $this->count) {
108
            $this->line('No journals to migrate to groups.');
109
        }
110
111
112
        $this->markAsMigrated();
113
114
115
        return 0;
116
    }
117
118
    /**
119
     * Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
120
     * executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
121
     * be called from the handle method instead of using the constructor to initialize the command.
122
     *
123
     * @codeCoverageIgnore
124
     */
125
    private function stupidLaravel(): void
126
    {
127
        $this->count             = 0;
128
        $this->journalRepository = app(JournalRepositoryInterface::class);
129
        $this->service           = app(JournalDestroyService::class);
130
        $this->groupFactory      = app(TransactionGroupFactory::class);
131
        $this->cliRepository     = app(JournalCLIRepositoryInterface::class);
132
    }
133
134
    /**
135
     * @param TransactionJournal $journal
136
     * @param Transaction $transaction
137
     *
138
     * @return Transaction|null
139
     */
140
    private function findOpposingTransaction(TransactionJournal $journal, Transaction $transaction): ?Transaction
141
    {
142
        $set = $journal->transactions->filter(
143
            static function (Transaction $subject) use ($transaction) {
144
                $amount     = (float)$transaction->amount * -1 === (float)$subject->amount;
145
                $identifier = $transaction->identifier === $subject->identifier;
146
                Log::debug(sprintf('Amount the same? %s', var_export($amount, true)));
147
                Log::debug(sprintf('ID the same?     %s', var_export($identifier, true)));
148
149
                return $amount && $identifier;
150
            }
151
        );
152
153
        return $set->first();
154
    }
155
156
    /**
157
     * @param TransactionJournal $journal
158
     *
159
     * @return Collection
160
     */
161
    private function getDestinationTransactions(TransactionJournal $journal): Collection
162
    {
163
        return $journal->transactions->filter(
164
            static function (Transaction $transaction) {
165
                return $transaction->amount > 0;
166
            }
167
        );
168
    }
169
170
    /**
171
     * @param array $array
172
     */
173
    private function giveGroup(array $array): void
174
    {
175
        $groupId = DB::table('transaction_groups')->insertGetId(
176
            [
177
                'created_at' => date('Y-m-d H:i:s'),
178
                'updated_at' => date('Y-m-d H:i:s'),
179
                'title'      => null,
180
                'user_id'    => $array['user_id'],
181
            ]
182
        );
183
        DB::table('transaction_journals')->where('id', $array['id'])->update(['transaction_group_id' => $groupId]);
184
        $this->count++;
185
    }
186
187
    /**
188
     * @return bool
189
     */
190
    private function isMigrated(): bool
191
    {
192
        $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false);
193
        if (null !== $configVar) {
194
            return (bool)$configVar->data;
195
        }
196
197
        return false; // @codeCoverageIgnore
198
    }
199
200
    /**
201
     * Gives all journals without a group a group.
202
     */
203
    private function makeGroupsFromAll(): void
204
    {
205
        $orphanedJournals = $this->cliRepository->getJournalsWithoutGroup();
206
        $count            = count($orphanedJournals);
207
        if ($count > 0) {
208
            Log::debug(sprintf('Going to convert %d transaction journals. Please hold..', $count));
209
            $this->line(sprintf('Going to convert %d transaction journals. Please hold..', $count));
210
            /** @var array $journal */
211
            foreach ($orphanedJournals as $array) {
212
                $this->giveGroup($array);
213
            }
214
        }
215
        if (0 === $count) {
216
            $this->info('No need to convert transaction journals.');
217
        }
218
    }
219
220
    /**
221
     * @throws Exception
222
     */
223
    private function makeGroupsFromSplitJournals(): void
224
    {
225
        $splitJournals = $this->cliRepository->getSplitJournals();
226
        if ($splitJournals->count() > 0) {
227
            $this->info(sprintf('Going to convert %d split transaction(s). Please hold..', $splitJournals->count()));
228
            /** @var TransactionJournal $journal */
229
            foreach ($splitJournals as $journal) {
230
                $this->makeMultiGroup($journal);
231
            }
232
        }
233
        if (0 === $splitJournals->count()) {
234
            $this->info('Found no split transaction journals. Nothing to do.');
235
        }
236
    }
237
238
    /**
239
     * @param TransactionJournal $journal
240
     *
241
     * @throws Exception
242
     *
243
     */
244
    private function makeMultiGroup(TransactionJournal $journal): void
245
    {
246
        // double check transaction count.
247
        if ($journal->transactions->count() <= 2) {
248
            // @codeCoverageIgnoreStart
249
            Log::debug(sprintf('Will not try to convert journal #%d because it has 2 or less transactions.', $journal->id));
250
251
            return;
252
            // @codeCoverageIgnoreEnd
253
        }
254
        Log::debug(sprintf('Will now try to convert journal #%d', $journal->id));
255
256
        $this->journalRepository->setUser($journal->user);
257
        $this->groupFactory->setUser($journal->user);
258
        $this->cliRepository->setUser($journal->user);
259
260
        $data             = [
261
            // mandatory fields.
262
            'group_title'  => $journal->description,
263
            'transactions' => [],
264
        ];
265
        $destTransactions = $this->getDestinationTransactions($journal);
266
        $budgetId         = $this->cliRepository->getJournalBudgetId($journal);
267
        $categoryId       = $this->cliRepository->getJournalCategoryId($journal);
268
        $notes            = $this->cliRepository->getNoteText($journal);
269
        $tags             = $this->cliRepository->getTags($journal);
270
        $internalRef      = $this->cliRepository->getMetaField($journal, 'internal-reference');
271
        $sepaCC           = $this->cliRepository->getMetaField($journal, 'sepa_cc');
272
        $sepaCtOp         = $this->cliRepository->getMetaField($journal, 'sepa_ct_op');
273
        $sepaCtId         = $this->cliRepository->getMetaField($journal, 'sepa_ct_id');
274
        $sepaDb           = $this->cliRepository->getMetaField($journal, 'sepa_db');
275
        $sepaCountry      = $this->cliRepository->getMetaField($journal, 'sepa_country');
276
        $sepaEp           = $this->cliRepository->getMetaField($journal, 'sepa_ep');
277
        $sepaCi           = $this->cliRepository->getMetaField($journal, 'sepa_ci');
278
        $sepaBatchId      = $this->cliRepository->getMetaField($journal, 'sepa_batch_id');
279
        $externalId       = $this->cliRepository->getMetaField($journal, 'external-id');
280
        $originalSource   = $this->cliRepository->getMetaField($journal, 'original-source');
281
        $recurrenceId     = $this->cliRepository->getMetaField($journal, 'recurrence_id');
282
        $bunq             = $this->cliRepository->getMetaField($journal, 'bunq_payment_id');
283
        $hash             = $this->cliRepository->getMetaField($journal, 'import_hash');
284
        $hashTwo          = $this->cliRepository->getMetaField($journal, 'import_hash_v2');
285
        $interestDate     = $this->cliRepository->getMetaDate($journal, 'interest_date');
286
        $bookDate         = $this->cliRepository->getMetaDate($journal, 'book_date');
287
        $processDate      = $this->cliRepository->getMetaDate($journal, 'process_date');
288
        $dueDate          = $this->cliRepository->getMetaDate($journal, 'due_date');
289
        $paymentDate      = $this->cliRepository->getMetaDate($journal, 'payment_date');
290
        $invoiceDate      = $this->cliRepository->getMetaDate($journal, 'invoice_date');
291
292
        Log::debug(sprintf('Will use %d positive transactions to create a new group.', $destTransactions->count()));
293
294
        /** @var Transaction $transaction */
295
        foreach ($destTransactions as $transaction) {
296
            Log::debug(sprintf('Now going to add transaction #%d to the array.', $transaction->id));
297
            $opposingTr = $this->findOpposingTransaction($journal, $transaction);
298
299
            if (null === $opposingTr) {
300
                // @codeCoverageIgnoreStart
301
                $this->error(
302
                    sprintf(
303
                        'Journal #%d has no opposing transaction for transaction #%d. Cannot upgrade this entry.',
304
                        $journal->id, $transaction->id
305
                    )
306
                );
307
                continue;
308
                // @codeCoverageIgnoreEnd
309
            }
310
311
            // overrule journal category with transaction category.
312
            $budgetId   = $this->getTransactionBudget($transaction, $opposingTr) ?? $budgetId;
313
            $categoryId = $this->getTransactionCategory($transaction, $opposingTr) ?? $categoryId;
314
315
            $tArray = [
316
                'type'                => strtolower($journal->transactionType->type),
317
                'date'                => $journal->date,
318
                'user'                => $journal->user_id,
319
                'currency_id'         => $transaction->transaction_currency_id,
320
                'foreign_currency_id' => $transaction->foreign_currency_id,
321
                'amount'              => $transaction->amount,
322
                'foreign_amount'      => $transaction->foreign_amount,
323
                'description'         => $transaction->description ?? $journal->description,
324
                'source_id'           => $opposingTr->account_id,
325
                'destination_id'      => $transaction->account_id,
326
                'budget_id'           => $budgetId,
327
                'category_id'         => $categoryId,
328
                'bill_id'             => $journal->bill_id,
329
                'notes'               => $notes,
330
                'tags'                => $tags,
331
                'internal_reference'  => $internalRef,
332
                'sepa_cc'             => $sepaCC,
333
                'sepa_ct_op'          => $sepaCtOp,
334
                'sepa_ct_id'          => $sepaCtId,
335
                'sepa_db'             => $sepaDb,
336
                'sepa_country'        => $sepaCountry,
337
                'sepa_ep'             => $sepaEp,
338
                'sepa_ci'             => $sepaCi,
339
                'sepa_batch_id'       => $sepaBatchId,
340
                'external_id'         => $externalId,
341
                'original-source'     => $originalSource,
342
                'recurrence_id'       => $recurrenceId,
343
                'bunq_payment_id'     => $bunq,
344
                'import_hash'         => $hash,
345
                'import_hash_v2'      => $hashTwo,
346
                'interest_date'       => $interestDate,
347
                'book_date'           => $bookDate,
348
                'process_date'        => $processDate,
349
                'due_date'            => $dueDate,
350
                'payment_date'        => $paymentDate,
351
                'invoice_date'        => $invoiceDate,
352
            ];
353
354
            $data['transactions'][] = $tArray;
355
        }
356
        Log::debug(sprintf('Now calling transaction journal factory (%d transactions in array)', count($data['transactions'])));
357
        $group = $this->groupFactory->create($data);
358
        Log::debug('Done calling transaction journal factory');
359
360
        // delete the old transaction journal.
361
        $this->service->destroy($journal);
362
363
        $this->count++;
364
365
        // report on result:
366
        Log::debug(
367
            sprintf('Migrated journal #%d into group #%d with these journals: #%s',
368
                    $journal->id, $group->id, implode(', #', $group->transactionJournals->pluck('id')->toArray()))
369
        );
370
        $this->line(
371
            sprintf('Migrated journal #%d into group #%d with these journals: #%s',
372
                    $journal->id, $group->id, implode(', #', $group->transactionJournals->pluck('id')->toArray()))
373
        );
374
    }
375
376
    /**
377
     * @param Transaction $left
378
     * @param Transaction $right
379
     *
380
     * @return int|null
381
     */
382
    private function getTransactionBudget(Transaction $left, Transaction $right): ?int
383
    {
384
        Log::debug('Now in getTransactionBudget()');
385
386
        // try to get a budget ID from the left transaction:
387
        /** @var Budget $budget */
388
        $budget = $left->budgets()->first();
389
        if (null !== $budget) {
390
            Log::debug(sprintf('Return budget #%d, from transaction #%d', $budget->id, $left->id));
391
392
            return (int)$budget->id;
393
        }
394
395
        // try to get a budget ID from the right transaction:
396
        /** @var Budget $budget */
397
        $budget = $right->budgets()->first();
398
        if (null !== $budget) {
399
            Log::debug(sprintf('Return budget #%d, from transaction #%d', $budget->id, $right->id));
400
401
            return (int)$budget->id;
402
        }
403
        Log::debug('Neither left or right have a budget, return NULL');
404
405
        // if all fails, return NULL.
406
        return null;
407
    }
408
409
    /**
410
     * @param Transaction $left
411
     * @param Transaction $right
412
     *
413
     * @return int|null
414
     */
415
    private function getTransactionCategory(Transaction $left, Transaction $right): ?int
416
    {
417
        Log::debug('Now in getTransactionCategory()');
418
419
        // try to get a category ID from the left transaction:
420
        /** @var Category $category */
421
        $category = $left->categories()->first();
422
        if (null !== $category) {
423
            Log::debug(sprintf('Return category #%d, from transaction #%d', $category->id, $left->id));
424
425
            return (int)$category->id;
426
        }
427
428
        // try to get a category ID from the left transaction:
429
        /** @var Category $category */
430
        $category = $right->categories()->first();
431
        if (null !== $category) {
432
            Log::debug(sprintf('Return category #%d, from transaction #%d', $category->id, $category->id));
433
434
            return (int)$category->id;
435
        }
436
        Log::debug('Neither left or right have a category, return NULL');
437
438
        // if all fails, return NULL.
439
        return null;
440
    }
441
442
    /**
443
     *
444
     */
445
    private function markAsMigrated(): void
446
    {
447
        app('fireflyconfig')->set(self::CONFIG_NAME, true);
448
    }
449
450
}
451