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.

TransferCurrenciesCorrections   F
last analyzed

Complexity

Total Complexity 64

Size/Duplication

Total Lines 562
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 64
eloc 225
dl 0
loc 562
rs 3.28
c 0
b 0
f 0

24 Methods

Rating   Name   Duplication   Size   Complexity  
A fixSourceNoCurrency() 0 15 3
A resetInformation() 0 8 1
A markAsExecuted() 0 3 1
A fixSourceUnmatchedCurrency() 0 18 4
A handle() 0 30 5
A fixDestNoCurrency() 0 15 3
A fixInvalidForeignCurrency() 0 24 2
A fixSourceNullForeignAmount() 0 11 3
A fixTransactionJournalCurrency() 0 16 2
A isSplitJournal() 0 3 1
A getSourceTransaction() 0 3 1
A fixDestinationUnmatchedCurrency() 0 18 4
A isEmptyTransactions() 0 5 4
A isNoCurrencyPresent() 0 25 3
A getCurrency() 0 22 6
A fixDestNullForeignAmount() 0 11 3
A stupidLaravel() 0 9 1
A startUpdateRoutine() 0 6 2
A updateTransferCurrency() 0 60 4
A fixMismatchedForeignCurrency() 0 12 2
A getDestinationTransaction() 0 3 1
A getDestinationInformation() 0 5 3
A getSourceInformation() 0 5 3
A isExecuted() 0 8 2

How to fix   Complexity   

Complex Class

Complex classes like TransferCurrenciesCorrections often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use TransferCurrenciesCorrections, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * TransferCurrenciesCorrections.php
4
 * Copyright (c) 2020 [email protected]
5
 *
6
 * This file is part of Firefly III (https://github.com/firefly-iii).
7
 *
8
 * This program is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU Affero General Public License as
10
 * published by the Free Software Foundation, either version 3 of the
11
 * License, or (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU Affero General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU Affero General Public License
19
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
20
 */
21
22
declare(strict_types=1);
23
24
namespace FireflyIII\Console\Commands\Upgrade;
25
26
use FireflyIII\Models\Account;
27
use FireflyIII\Models\Transaction;
28
use FireflyIII\Models\TransactionCurrency;
29
use FireflyIII\Models\TransactionJournal;
30
use FireflyIII\Models\TransactionType;
31
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
32
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
33
use FireflyIII\Repositories\Journal\JournalCLIRepositoryInterface;
34
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
35
use Illuminate\Console\Command;
36
use Log;
37
38
/**
39
 * Class TransferCurrenciesCorrections
40
 */
41
class TransferCurrenciesCorrections extends Command
42
{
43
    public const CONFIG_NAME = '480_transfer_currencies';
44
    /**
45
     * The console command description.
46
     *
47
     * @var string
48
     */
49
    protected $description = 'Updates transfer currency information.';
50
    /**
51
     * The name and signature of the console command.
52
     *
53
     * @var string
54
     */
55
    protected $signature = 'firefly-iii:transfer-currencies {--F|force : Force the execution of this command.}';
56
    /** @var array */
57
    private $accountCurrencies;
58
    /** @var AccountRepositoryInterface */
59
    private $accountRepos;
60
    /** @var JournalCLIRepositoryInterface */
61
    private $cliRepos;
62
    /** @var int */
63
    private $count;
64
    /** @var CurrencyRepositoryInterface */
65
    private $currencyRepos;
66
    /** @var Account The destination account of the current journal. */
67
    private $destinationAccount;
68
    /** @var TransactionCurrency The currency preference of the destination account of the current journal. */
69
    private $destinationCurrency;
70
    /** @var Transaction The destination transaction of the current journal. */
71
    private $destinationTransaction;
72
    /** @var JournalRepositoryInterface */
73
    private $journalRepos;
74
    /** @var Account The source account of the current journal. */
75
    private $sourceAccount;
76
    /** @var TransactionCurrency The currency preference of the source account of the current journal. */
77
    private $sourceCurrency;
78
    /** @var Transaction The source transaction of the current journal. */
79
    private $sourceTransaction;
80
81
    /**
82
     * Execute the console command.
83
     *
84
     * @return int
85
     */
86
    public function handle(): int
87
    {
88
        $this->stupidLaravel();
89
        $start = microtime(true);
90
        // @codeCoverageIgnoreStart
91
        if ($this->isExecuted() && true !== $this->option('force')) {
92
            $this->warn('This command has already been executed.');
93
94
            return 0;
95
        }
96
        // @codeCoverageIgnoreEnd
97
98
        $this->startUpdateRoutine();
99
        $this->markAsExecuted();
100
101
        if (0 === $this->count) {
102
            $message = 'All transfers have correct currency information.';
103
            $this->line($message);
104
            Log::debug($message);
105
        }
106
        if (0 !== $this->count) {
107
            $message = sprintf('Verified currency information of %d transfer(s).', $this->count);
108
            $this->line($message);
109
            Log::debug($message);
110
        }
111
        $end = round(microtime(true) - $start, 2);
112
        $this->info(sprintf('Verified and fixed currency information for transfers in %s seconds.', $end));
113
114
        // app('telemetry')->feature('executed-command', $this->signature);
115
        return 0;
116
    }
117
118
    /**
119
     * The destination transaction must have a currency. If not, it will be added by
120
     * taking it from the destination account's preference.
121
     */
122
    private function fixDestNoCurrency(): void
123
    {
124
        if (null === $this->destinationTransaction->transaction_currency_id && null !== $this->destinationCurrency) {
125
            $this->destinationTransaction
126
                ->transaction_currency_id
127
                     = (int) $this->destinationCurrency->id;
128
            $message = sprintf(
129
                'Transaction #%d has no currency setting, now set to %s.',
130
                $this->destinationTransaction->id,
131
                $this->destinationCurrency->code
132
            );
133
            Log::debug($message);
134
            $this->line($message);
135
            $this->count++;
136
            $this->destinationTransaction->save();
137
        }
138
    }
139
140
    /**
141
     * If the foreign amount of the destination transaction is null, but that of the other isn't, use this piece of code
142
     * to restore it.
143
     */
144
    private function fixDestNullForeignAmount(): void
145
    {
146
        if (null === $this->destinationTransaction->foreign_amount && null !== $this->sourceTransaction->foreign_amount) {
147
            $this->destinationTransaction->foreign_amount = bcmul((string) $this->sourceTransaction->foreign_amount, '-1');
148
            $this->destinationTransaction->save();
149
            $this->count++;
150
            Log::debug(
151
                sprintf(
152
                    'Restored foreign amount of destination transaction #%d to %s',
153
                    $this->destinationTransaction->id,
154
                    $this->destinationTransaction->foreign_amount
155
                )
156
            );
157
        }
158
    }
159
160
    /**
161
     * The destination transaction must have the correct currency. If not, it will be set by
162
     * taking it from the destination account's preference.
163
     */
164
    private function fixDestinationUnmatchedCurrency(): void
165
    {
166
        if (null !== $this->destinationCurrency
167
            && null === $this->destinationTransaction->foreign_amount
168
            && (int) $this->destinationTransaction->transaction_currency_id !== (int) $this->destinationCurrency->id
169
        ) {
170
            $message = sprintf(
171
                'Transaction #%d has a currency setting #%d that should be #%d. Amount remains %s, currency is changed.',
172
                $this->destinationTransaction->id,
173
                $this->destinationTransaction->transaction_currency_id,
174
                $this->destinationAccount->id,
175
                $this->destinationTransaction->amount
176
            );
177
            Log::debug($message);
178
            $this->line($message);
179
            $this->count++;
180
            $this->destinationTransaction->transaction_currency_id = (int) $this->destinationCurrency->id;
181
            $this->destinationTransaction->save();
182
        }
183
    }
184
185
    /**
186
     * If the destination account currency is the same as the source currency,
187
     * both foreign_amount and foreign_currency_id fields must be NULL
188
     * for both transactions (because foreign currency info would not make sense)
189
     *
190
     */
191
    private function fixInvalidForeignCurrency(): void
192
    {
193
        if ((int) $this->destinationCurrency->id === (int) $this->sourceCurrency->id) {
194
            // update both transactions to match:
195
            $this->sourceTransaction->foreign_amount      = null;
196
            $this->sourceTransaction->foreign_currency_id = null;
197
198
            $this->destinationTransaction->foreign_amount      = null;
199
            $this->destinationTransaction->foreign_currency_id = null;
200
201
            $this->sourceTransaction->save();
202
            $this->destinationTransaction->save();
203
204
            Log::debug(
205
                sprintf(
206
                    'Currency for account "%s" is %s, and currency for account "%s" is also
207
             %s, so transactions #%d and #%d has been verified to be to %s exclusively.',
208
                    $this->destinationAccount->name,
209
                    $this->destinationCurrency->code,
210
                    $this->sourceAccount->name,
211
                    $this->sourceCurrency->code,
212
                    $this->sourceTransaction->id,
213
                    $this->destinationTransaction->id,
214
                    $this->sourceCurrency->code
215
                )
216
            );
217
        }
218
    }
219
220
    /**
221
     * If destination account currency is different from source account currency,
222
     * then both transactions must get the source account's currency as normal currency
223
     * and the opposing account's currency as foreign currency.
224
     */
225
    private function fixMismatchedForeignCurrency(): void
226
    {
227
        if ((int) $this->sourceCurrency->id !== (int) $this->destinationCurrency->id) {
228
            $this->sourceTransaction->transaction_currency_id      = $this->sourceCurrency->id;
229
            $this->sourceTransaction->foreign_currency_id          = $this->destinationCurrency->id;
230
            $this->destinationTransaction->transaction_currency_id = $this->sourceCurrency->id;
231
            $this->destinationTransaction->foreign_currency_id     = $this->destinationCurrency->id;
232
233
            $this->sourceTransaction->save();
234
            $this->destinationTransaction->save();
235
            $this->count++;
236
            Log::debug(sprintf('Verified foreign currency ID of transaction #%d and #%d', $this->sourceTransaction->id, $this->destinationTransaction->id));
237
        }
238
    }
239
240
    /**
241
     * The source transaction must have a currency. If not, it will be added by
242
     * taking it from the source account's preference.
243
     */
244
    private function fixSourceNoCurrency(): void
245
    {
246
        if (null === $this->sourceTransaction->transaction_currency_id && null !== $this->sourceCurrency) {
247
            $this->sourceTransaction
248
                ->transaction_currency_id
249
                     = (int) $this->sourceCurrency->id;
250
            $message = sprintf(
251
                'Transaction #%d has no currency setting, now set to %s.',
252
                $this->sourceTransaction->id,
253
                $this->sourceCurrency->code
254
            );
255
            Log::debug($message);
256
            $this->line($message);
257
            $this->count++;
258
            $this->sourceTransaction->save();
259
        }
260
    }
261
262
    /**
263
     * If the foreign amount of the source transaction is null, but that of the other isn't, use this piece of code
264
     * to restore it.
265
     */
266
    private function fixSourceNullForeignAmount(): void
267
    {
268
        if (null === $this->sourceTransaction->foreign_amount && null !== $this->destinationTransaction->foreign_amount) {
269
            $this->sourceTransaction->foreign_amount = bcmul((string) $this->destinationTransaction->foreign_amount, '-1');
270
            $this->sourceTransaction->save();
271
            $this->count++;
272
            Log::debug(
273
                sprintf(
274
                    'Restored foreign amount of source transaction #%d to %s',
275
                    $this->sourceTransaction->id,
276
                    $this->sourceTransaction->foreign_amount
277
                )
278
            );
279
        }
280
    }
281
282
    /**
283
     * The source transaction must have the correct currency. If not, it will be set by
284
     * taking it from the source account's preference.
285
     */
286
    private function fixSourceUnmatchedCurrency(): void
287
    {
288
        if (null !== $this->sourceCurrency
289
            && null === $this->sourceTransaction->foreign_amount
290
            && (int) $this->sourceTransaction->transaction_currency_id !== (int) $this->sourceCurrency->id
291
        ) {
292
            $message = sprintf(
293
                'Transaction #%d has a currency setting #%d that should be #%d. Amount remains %s, currency is changed.',
294
                $this->sourceTransaction->id,
295
                $this->sourceTransaction->transaction_currency_id,
296
                $this->sourceAccount->id,
297
                $this->sourceTransaction->amount
298
            );
299
            Log::debug($message);
300
            $this->line($message);
301
            $this->count++;
302
            $this->sourceTransaction->transaction_currency_id = (int) $this->sourceCurrency->id;
303
            $this->sourceTransaction->save();
304
        }
305
    }
306
307
    /**
308
     * This method makes sure that the transaction journal uses the currency given in the source transaction.
309
     *
310
     * @param TransactionJournal $journal
311
     */
312
    private function fixTransactionJournalCurrency(TransactionJournal $journal): void
313
    {
314
        if ((int) $journal->transaction_currency_id !== (int) $this->sourceCurrency->id) {
315
            $oldCurrencyCode                  = $journal->transactionCurrency->code ?? '(nothing)';
316
            $journal->transaction_currency_id = $this->sourceCurrency->id;
317
            $message                          = sprintf(
318
                'Transfer #%d ("%s") has been updated to use %s instead of %s.',
319
                $journal->id,
320
                $journal->description,
321
                $this->sourceCurrency->code,
322
                $oldCurrencyCode
323
            );
324
            $this->count++;
325
            $this->line($message);
326
            Log::debug($message);
327
            $journal->save();
328
        }
329
    }
330
331
    /**
332
     * @param Account $account
333
     *
334
     * @return TransactionCurrency|null
335
     */
336
    private function getCurrency(Account $account): ?TransactionCurrency
337
    {
338
        $accountId = $account->id;
339
        if (isset($this->accountCurrencies[$accountId]) && 0 === $this->accountCurrencies[$accountId]) {
340
            return null; // @codeCoverageIgnore
341
        }
342
        if (isset($this->accountCurrencies[$accountId]) && $this->accountCurrencies[$accountId] instanceof TransactionCurrency) {
343
            return $this->accountCurrencies[$accountId]; // @codeCoverageIgnore
344
        }
345
        // TODO we can use getAccountCurrency() instead
346
        $currencyId = (int) $this->accountRepos->getMetaValue($account, 'currency_id');
347
        $result     = $this->currencyRepos->findNull($currencyId);
348
        if (null === $result) {
349
            // @codeCoverageIgnoreStart
350
            $this->accountCurrencies[$accountId] = 0;
351
352
            return null;
353
            // @codeCoverageIgnoreEnd
354
        }
355
        $this->accountCurrencies[$accountId] = $result;
356
357
        return $result;
358
    }
359
360
    /**
361
     * Extract destination transaction, destination account + destination account currency from the journal.
362
     *
363
     * @param TransactionJournal $journal
364
     *
365
     * @codeCoverageIgnore
366
     */
367
    private function getDestinationInformation(TransactionJournal $journal): void
368
    {
369
        $this->destinationTransaction = $this->getDestinationTransaction($journal);
370
        $this->destinationAccount     = null === $this->destinationTransaction ? null : $this->destinationTransaction->account;
371
        $this->destinationCurrency    = null === $this->destinationAccount ? null : $this->getCurrency($this->destinationAccount);
372
    }
373
374
    /**
375
     * @param TransactionJournal $transfer
376
     *
377
     * @return Transaction|null
378
     * @codeCoverageIgnore
379
     */
380
    private function getDestinationTransaction(TransactionJournal $transfer): ?Transaction
381
    {
382
        return $transfer->transactions()->where('amount', '>', 0)->first();
383
    }
384
385
    /**
386
     * Extract source transaction, source account + source account currency from the journal.
387
     *
388
     * @param TransactionJournal $journal
389
     *
390
     * @codeCoverageIgnore
391
     */
392
    private function getSourceInformation(TransactionJournal $journal): void
393
    {
394
        $this->sourceTransaction = $this->getSourceTransaction($journal);
395
        $this->sourceAccount     = null === $this->sourceTransaction ? null : $this->sourceTransaction->account;
396
        $this->sourceCurrency    = null === $this->sourceAccount ? null : $this->getCurrency($this->sourceAccount);
397
    }
398
399
    /**
400
     * @param TransactionJournal $transfer
401
     *
402
     * @return Transaction|null
403
     * @codeCoverageIgnore
404
     */
405
    private function getSourceTransaction(TransactionJournal $transfer): ?Transaction
406
    {
407
        return $transfer->transactions()->where('amount', '<', 0)->first();
408
    }
409
410
    /**
411
     * Is either the source or destination transaction NULL?
412
     *
413
     * @return bool
414
     * @codeCoverageIgnore
415
     */
416
    private function isEmptyTransactions(): bool
417
    {
418
        return null === $this->sourceTransaction || null === $this->destinationTransaction
419
               || null === $this->sourceAccount
420
               || null === $this->destinationAccount;
421
    }
422
423
    /**
424
     * @return bool
425
     */
426
    private function isExecuted(): bool
427
    {
428
        $configVar = app('fireflyconfig')->get(self::CONFIG_NAME, false);
429
        if (null !== $configVar) {
430
            return (bool) $configVar->data;
431
        }
432
433
        return false; // @codeCoverageIgnore
434
    }
435
436
    /**
437
     * @return bool
438
     * @codeCoverageIgnore
439
     */
440
    private function isNoCurrencyPresent(): bool
441
    {
442
        // source account must have a currency preference.
443
        if (null === $this->sourceCurrency) {
444
            $message = sprintf('Account #%d ("%s") must have currency preference but has none.', $this->sourceAccount->id, $this->sourceAccount->name);
445
            Log::error($message);
446
            $this->error($message);
447
448
            return true;
449
        }
450
451
        // destination account must have a currency preference.
452
        if (null === $this->destinationCurrency) {
453
            $message = sprintf(
454
                'Account #%d ("%s") must have currency preference but has none.',
455
                $this->destinationAccount->id,
456
                $this->destinationAccount->name
457
            );
458
            Log::error($message);
459
            $this->error($message);
460
461
            return true;
462
        }
463
464
        return false;
465
    }
466
467
    /**
468
     * Is this a split transaction journal?
469
     *
470
     * @param TransactionJournal $transfer
471
     *
472
     * @return bool
473
     * @codeCoverageIgnore
474
     */
475
    private function isSplitJournal(TransactionJournal $transfer): bool
476
    {
477
        return $transfer->transactions->count() > 2;
478
    }
479
480
    /**
481
     *
482
     */
483
    private function markAsExecuted(): void
484
    {
485
        app('fireflyconfig')->set(self::CONFIG_NAME, true);
486
    }
487
488
    /**
489
     * Reset all the class fields for the current transfer.
490
     *
491
     * @codeCoverageIgnore
492
     */
493
    private function resetInformation(): void
494
    {
495
        $this->sourceTransaction      = null;
496
        $this->sourceAccount          = null;
497
        $this->sourceCurrency         = null;
498
        $this->destinationTransaction = null;
499
        $this->destinationAccount     = null;
500
        $this->destinationCurrency    = null;
501
    }
502
503
    /**
504
     * This routine verifies that transfers have the correct currency settings for the accounts they are linked to.
505
     * For transfers, this is can be a destructive routine since we FORCE them into a currency setting whether they
506
     * like it or not. Previous routines MUST have set the currency setting for both accounts for this to work.
507
     *
508
     * A transfer always has the
509
     *
510
     * Both source and destination must match the respective currency preference. So FF3 must verify ALL
511
     * transactions.
512
     */
513
    private function startUpdateRoutine(): void
514
    {
515
        $set = $this->cliRepos->getAllJournals([TransactionType::TRANSFER]);
516
        /** @var TransactionJournal $journal */
517
        foreach ($set as $journal) {
518
            $this->updateTransferCurrency($journal);
519
        }
520
    }
521
522
    /**
523
     * Laravel will execute ALL __construct() methods for ALL commands whenever a SINGLE command is
524
     * executed. This leads to noticeable slow-downs and class calls. To prevent this, this method should
525
     * be called from the handle method instead of using the constructor to initialize the command.
526
     *
527
     * @codeCoverageIgnore
528
     */
529
    private function stupidLaravel(): void
530
    {
531
        $this->count             = 0;
532
        $this->accountRepos      = app(AccountRepositoryInterface::class);
533
        $this->currencyRepos     = app(CurrencyRepositoryInterface::class);
534
        $this->journalRepos      = app(JournalRepositoryInterface::class);
535
        $this->cliRepos          = app(JournalCLIRepositoryInterface::class);
536
        $this->accountCurrencies = [];
537
        $this->resetInformation();
538
    }
539
540
    /**
541
     * @param TransactionJournal $transfer
542
     */
543
    private function updateTransferCurrency(TransactionJournal $transfer): void
544
    {
545
        $this->resetInformation();
546
547
        // @codeCoverageIgnoreStart
548
        if ($this->isSplitJournal($transfer)) {
549
            $this->line(sprintf(sprintf('Transaction journal #%d is a split journal. Cannot continue.', $transfer->id)));
550
551
            return;
552
        }
553
        // @codeCoverageIgnoreEnd
554
555
        $this->getSourceInformation($transfer);
556
        $this->getDestinationInformation($transfer);
557
558
        // unexpectedly, either one is null:
559
        // @codeCoverageIgnoreStart
560
        if ($this->isEmptyTransactions()) {
561
            $this->error(sprintf('Source or destination information for transaction journal #%d is null. Cannot fix this one.', $transfer->id));
562
563
            return;
564
        }
565
        // @codeCoverageIgnoreEnd
566
567
568
        // both accounts must have currency preference:
569
        // @codeCoverageIgnoreStart
570
        if ($this->isNoCurrencyPresent()) {
571
            $this->error(
572
                sprintf('Source or destination accounts for transaction journal #%d have no currency information. Cannot fix this one.', $transfer->id)
573
            );
574
575
            return;
576
        }
577
        // @codeCoverageIgnoreEnd
578
579
        // fix source transaction having no currency.
580
        $this->fixSourceNoCurrency();
581
582
        // fix source transaction having bad currency.
583
        $this->fixSourceUnmatchedCurrency();
584
585
        // fix destination transaction having no currency.
586
        $this->fixDestNoCurrency();
587
588
        // fix destination transaction having bad currency.
589
        $this->fixDestinationUnmatchedCurrency();
590
591
        // remove foreign currency information if not necessary.
592
        $this->fixInvalidForeignCurrency();
593
        // correct foreign currency info if necessary.
594
        $this->fixMismatchedForeignCurrency();
595
596
        // restore missing foreign currency amount.
597
        $this->fixSourceNullForeignAmount();
598
599
        $this->fixDestNullForeignAmount();
600
601
        // fix journal itself:
602
        $this->fixTransactionJournalCurrency($transfer);
603
    }
604
}
605