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 ( d8807e...292fb3 )
by James
41:34 queued 29:54
created

PeriodOverview::groupByCurrency()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 41
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 28
c 1
b 0
f 0
dl 0
loc 41
rs 8.8497
cc 6
nc 7
nop 1
1
<?php
2
/**
3
 * PeriodOverview.php
4
 * Copyright (c) 2019 [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\Support\Http\Controllers;
25
26
use Carbon\Carbon;
27
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
28
use FireflyIII\Models\Account;
29
use FireflyIII\Models\Category;
30
use FireflyIII\Models\Tag;
31
use FireflyIII\Models\TransactionType;
32
use FireflyIII\Support\CacheProperties;
33
use Illuminate\Support\Collection;
34
use Log;
35
36
/**
37
 * Trait PeriodOverview.
38
 *
39
 * TODO verify this all works as expected.
40
 *
41
 * - Always request start date and end date.
42
 * - Group expenses, income, etc. under this period.
43
 * - Returns collection of arrays. Fields
44
 *      title (string),
45
 *      route (string)
46
 *      total_transactions (int)
47
 *      spent (array),
48
 *      earned (array),
49
 *      transferred_away (array)
50
 *      transferred_in (array)
51
 *      transferred (array)
52
 *
53
 * each array has the following format:
54
 * currency_id => [
55
 *       currency_id : 1, (int)
56
 *       currency_symbol : X (str)
57
 *       currency_name: Euro (str)
58
 *       currency_code: EUR (str)
59
 *       amount: -1234 (str)
60
 *       count: 23
61
 *       ]
62
 *
63
 */
64
trait PeriodOverview
65
{
66
67
    /**
68
     * This method returns "period entries", so nov-2015, dec-2015, etc etc (this depends on the users session range)
69
     * and for each period, the amount of money spent and earned. This is a complex operation which is cached for
70
     * performance reasons.
71
     *
72
     * @param Account $account The account involved
73
     * @param Carbon $date The start date.
74
     * @param Carbon $end The end date.
75
     *
76
     * @return array
77
     */
78
    protected function getAccountPeriodOverview(Account $account, Carbon $start, Carbon $end): array
79
    {
80
        $range = app('preferences')->get('viewRange', '1M')->data;
81
        [$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
82
83
        // properties for cache
84
        $cache = new CacheProperties;
85
        $cache->addProperty($start);
86
        $cache->addProperty($end);
87
        $cache->addProperty('account-show-period-entries');
88
        $cache->addProperty($account->id);
89
        if ($cache->has()) {
90
            return $cache->get(); // @codeCoverageIgnore
91
        }
92
        /** @var array $dates */
93
        $dates   = app('navigation')->blockPeriods($start, $end, $range);
94
        $entries = [];
95
96
        // collect all expenses in this period:
97
        /** @var GroupCollectorInterface $collector */
98
        $collector = app(GroupCollectorInterface::class);
99
        $collector->setAccounts(new Collection([$account]));
100
        $collector->setRange($start, $end);
101
        $collector->setTypes([TransactionType::DEPOSIT]);
102
        $earnedSet = $collector->getExtractedJournals();
103
104
        // collect all income in this period:
105
        /** @var GroupCollectorInterface $collector */
106
        $collector = app(GroupCollectorInterface::class);
107
        $collector->setAccounts(new Collection([$account]));
108
        $collector->setRange($start, $end);
109
        $collector->setTypes([TransactionType::WITHDRAWAL]);
110
        $spentSet = $collector->getExtractedJournals();
111
112
        // collect all transfers in this period:
113
        /** @var GroupCollectorInterface $collector */
114
        $collector = app(GroupCollectorInterface::class);
115
        $collector->setAccounts(new Collection([$account]));
116
        $collector->setRange($start, $end);
117
        $collector->setTypes([TransactionType::TRANSFER]);
118
        $transferSet = $collector->getExtractedJournals();
119
120
        // loop dates
121
        foreach ($dates as $currentDate) {
122
            $title           = app('navigation')->periodShow($currentDate['start'], $currentDate['period']);
123
            $earned          = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']);
124
            $spent           = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']);
125
            $transferredAway = $this->filterTransferredAway($account, $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']));
126
            $transferredIn   = $this->filterTransferredIn($account, $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']));
127
            $entries[]       =
128
                [
129
                    'title' => $title,
130
                    'route' =>
131
                        route('accounts.show', [$account->id, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
132
133
                    'total_transactions' => count($spent) + count($earned) + count($transferredAway) + count($transferredIn),
134
                    'spent'              => $this->groupByCurrency($spent),
135
                    'earned'             => $this->groupByCurrency($earned),
136
                    'transferred_away'   => $this->groupByCurrency($transferredAway),
137
                    'transferred_in'     => $this->groupByCurrency($transferredIn),
138
                ];
139
        }
140
        $cache->store($entries);
141
142
        return $entries;
143
    }
144
145
    /**
146
     * Overview for single category. Has been refactored recently.
147
     *
148
     * @param Category $category
149
     * @param Carbon $start
150
     * @param Carbon $end
151
     * @return array
152
     */
153
    protected function getCategoryPeriodOverview(Category $category, Carbon $start, Carbon $end): array
154
    {
155
        $range = app('preferences')->get('viewRange', '1M')->data;
156
        [$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
157
158
        // properties for entries with their amounts.
159
        $cache = new CacheProperties();
160
        $cache->addProperty($start);
161
        $cache->addProperty($end);
162
        $cache->addProperty($range);
163
        $cache->addProperty('category-show-period-entries');
164
        $cache->addProperty($category->id);
165
166
        if ($cache->has()) {
167
            return $cache->get(); // @codeCoverageIgnore
168
        }
169
        /** @var array $dates */
170
        $dates   = app('navigation')->blockPeriods($start, $end, $range);
171
        $entries = [];
172
173
        // collect all expenses in this period:
174
        /** @var GroupCollectorInterface $collector */
175
        $collector = app(GroupCollectorInterface::class);
176
        $collector->setCategory($category);
177
        $collector->setRange($start, $end);
178
        $collector->setTypes([TransactionType::DEPOSIT]);
179
        $earnedSet = $collector->getExtractedJournals();
180
181
        // collect all income in this period:
182
        /** @var GroupCollectorInterface $collector */
183
        $collector = app(GroupCollectorInterface::class);
184
        $collector->setCategory($category);
185
        $collector->setRange($start, $end);
186
        $collector->setTypes([TransactionType::WITHDRAWAL]);
187
        $spentSet = $collector->getExtractedJournals();
188
189
        // collect all transfers in this period:
190
        /** @var GroupCollectorInterface $collector */
191
        $collector = app(GroupCollectorInterface::class);
192
        $collector->setCategory($category);
193
        $collector->setRange($start, $end);
194
        $collector->setTypes([TransactionType::TRANSFER]);
195
        $transferSet = $collector->getExtractedJournals();
196
197
198
        foreach ($dates as $currentDate) {
199
            $spent       = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']);
200
            $earned      = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']);
201
            $transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']);
202
            $title       = app('navigation')->periodShow($currentDate['end'], $currentDate['period']);
203
            $entries[]   =
204
                [
205
                    'transactions'       => 0,
206
                    'title'              => $title,
207
                    'route'              => route('categories.show',
208
                                                  [$category->id, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
209
                    'total_transactions' => count($spent) + count($earned) + count($transferred),
210
                    'spent'              => $this->groupByCurrency($spent),
211
                    'earned'             => $this->groupByCurrency($earned),
212
                    'transferred'        => $this->groupByCurrency($transferred),
213
                ];
214
        }
215
        $cache->store($entries);
216
217
        return $entries;
218
    }
219
220
    /**
221
     * Same as above, but for lists that involve transactions without a budget.
222
     *
223
     * This method has been refactored recently.
224
     *
225
     * @param Carbon $start
226
     * @param Carbon $date
227
     *
228
     * @return array
229
     */
230
    protected function getNoBudgetPeriodOverview(Carbon $start, Carbon $end): array
231
    {
232
        $range = app('preferences')->get('viewRange', '1M')->data;
233
234
        [$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
235
236
        $cache = new CacheProperties;
237
        $cache->addProperty($start);
238
        $cache->addProperty($end);
239
        $cache->addProperty('no-budget-period-entries');
240
241
        if ($cache->has()) {
242
            return $cache->get(); // @codeCoverageIgnore
243
        }
244
245
        /** @var array $dates */
246
        $dates   = app('navigation')->blockPeriods($start, $end, $range);
247
        $entries = [];
248
249
250
        // get all expenses without a budget.
251
        /** @var GroupCollectorInterface $collector */
252
        $collector = app(GroupCollectorInterface::class);
253
        $collector->setRange($start, $end)->withoutBudget()->withAccountInformation()->setTypes([TransactionType::WITHDRAWAL]);
254
        $journals = $collector->getExtractedJournals();
255
256
        foreach ($dates as $currentDate) {
257
            $set       = $this->filterJournalsByDate($journals, $currentDate['start'], $currentDate['end']);
258
            $title     = app('navigation')->periodShow($currentDate['end'], $currentDate['period']);
259
            $entries[] =
260
                [
261
                    'title'              => $title,
262
                    'route'              => route('budgets.no-budget', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
263
                    'total_transactions' => count($set),
264
                    'spent'              => $this->groupByCurrency($set),
265
                    'earned'             => [],
266
                    'transferred_away'   => [],
267
                    'transferred_in'     => [],
268
                ];
269
        }
270
        $cache->store($entries);
271
272
        return $entries;
273
    }
274
275
    /**
276
     * TODO fix date.
277
     *
278
     * Show period overview for no category view.
279
     *
280
     * @param Carbon $theDate
281
     *
282
     * @return array
283
     *
284
     */
285
    protected function getNoCategoryPeriodOverview(Carbon $theDate): array
286
    {
287
        Log::debug(sprintf('Now in getNoCategoryPeriodOverview(%s)', $theDate->format('Y-m-d')));
288
        $range = app('preferences')->get('viewRange', '1M')->data;
289
        $first = $this->journalRepos->firstNull();
290
        $start = null === $first ? new Carbon : $first->date;
291
        $end   = $theDate ?? new Carbon;
292
293
        Log::debug(sprintf('Start for getNoCategoryPeriodOverview() is %s', $start->format('Y-m-d')));
294
        Log::debug(sprintf('End for getNoCategoryPeriodOverview() is %s', $end->format('Y-m-d')));
295
296
        // properties for cache
297
        $cache = new CacheProperties;
298
        $cache->addProperty($start);
299
        $cache->addProperty($end);
300
        $cache->addProperty('no-category-period-entries');
301
302
        if ($cache->has()) {
303
            return $cache->get(); // @codeCoverageIgnore
304
        }
305
306
        $dates   = app('navigation')->blockPeriods($start, $end, $range);
307
        $entries = [];
308
309
        // collect all expenses in this period:
310
        /** @var GroupCollectorInterface $collector */
311
        $collector = app(GroupCollectorInterface::class);
312
        $collector->withoutCategory();
313
        $collector->setRange($start, $end);
314
        $collector->setTypes([TransactionType::DEPOSIT]);
315
        $earnedSet = $collector->getExtractedJournals();
316
317
        // collect all income in this period:
318
        /** @var GroupCollectorInterface $collector */
319
        $collector = app(GroupCollectorInterface::class);
320
        $collector->withoutCategory();
321
        $collector->setRange($start, $end);
322
        $collector->setTypes([TransactionType::WITHDRAWAL]);
323
        $spentSet = $collector->getExtractedJournals();
324
325
        // collect all transfers in this period:
326
        /** @var GroupCollectorInterface $collector */
327
        $collector = app(GroupCollectorInterface::class);
328
        $collector->withoutCategory();
329
        $collector->setRange($start, $end);
330
        $collector->setTypes([TransactionType::TRANSFER]);
331
        $transferSet = $collector->getExtractedJournals();
332
333
        /** @var array $currentDate */
334
        foreach ($dates as $currentDate) {
335
            $spent       = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']);
336
            $earned      = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']);
337
            $transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']);
338
            $title       = app('navigation')->periodShow($currentDate['end'], $currentDate['period']);
339
            $entries[]   =
340
                [
341
                    'title'              => $title,
342
                    'route'              => route('categories.no-category', [$currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
343
                    'total_transactions' => count($spent) + count($earned) + count($transferred),
344
                    'spent'              => $this->groupByCurrency($spent),
345
                    'earned'             => $this->groupByCurrency($earned),
346
                    'transferred'        => $this->groupByCurrency($transferred),
347
                ];
348
        }
349
        Log::debug('End of loops');
350
        $cache->store($entries);
351
352
        return $entries;
353
    }
354
355
    /**
356
     * This shows a period overview for a tag. It goes back in time and lists all relevant transactions and sums.
357
     *
358
     * @param Tag $tag
359
     *
360
     * @param Carbon $date
361
     *
362
     * @return array
363
     */
364
    protected function getTagPeriodOverview(Tag $tag, Carbon $start, Carbon $end): array // period overview for tags.
365
    {
366
367
        $range = app('preferences')->get('viewRange', '1M')->data;
368
        [$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
369
370
        // properties for cache
371
        $cache = new CacheProperties;
372
        $cache->addProperty($start);
373
        $cache->addProperty($end);
374
        $cache->addProperty('tag-period-entries');
375
        $cache->addProperty($tag->id);
376
        if ($cache->has()) {
377
             return $cache->get(); // @codeCoverageIgnore
378
        }
379
        /** @var array $dates */
380
        $dates   = app('navigation')->blockPeriods($start, $end, $range);
381
        $entries = [];
382
383
        // collect all expenses in this period:
384
        /** @var GroupCollectorInterface $collector */
385
        $collector = app(GroupCollectorInterface::class);
386
        $collector->setTag($tag);
387
        $collector->setRange($start, $end);
388
        $collector->setTypes([TransactionType::DEPOSIT]);
389
        $earnedSet = $collector->getExtractedJournals();
390
391
        // collect all income in this period:
392
        /** @var GroupCollectorInterface $collector */
393
        $collector = app(GroupCollectorInterface::class);
394
        $collector->setTag($tag);
395
        $collector->setRange($start, $end);
396
        $collector->setTypes([TransactionType::WITHDRAWAL]);
397
        $spentSet = $collector->getExtractedJournals();
398
399
        // collect all transfers in this period:
400
        /** @var GroupCollectorInterface $collector */
401
        $collector = app(GroupCollectorInterface::class);
402
        $collector->setTag($tag);
403
        $collector->setRange($start, $end);
404
        $collector->setTypes([TransactionType::TRANSFER]);
405
        $transferSet = $collector->getExtractedJournals();
406
407
        foreach ($dates as $currentDate) {
408
            $spent       = $this->filterJournalsByDate($spentSet, $currentDate['start'], $currentDate['end']);
409
            $earned      = $this->filterJournalsByDate($earnedSet, $currentDate['start'], $currentDate['end']);
410
            $transferred = $this->filterJournalsByDate($transferSet, $currentDate['start'], $currentDate['end']);
411
            $title       = app('navigation')->periodShow($currentDate['end'], $currentDate['period']);
412
            $entries[]   =
413
                [
414
                    'transactions'       => 0,
415
                    'title'              => $title,
416
                    'route'              => route('tags.show',
417
                                                  [$tag->id, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
418
                    'total_transactions' => count($spent) + count($earned) + count($transferred),
419
                    'spent'              => $this->groupByCurrency($spent),
420
                    'earned'             => $this->groupByCurrency($earned),
421
                    'transferred'        => $this->groupByCurrency($transferred),
422
                ];
423
        }
424
425
        return $entries;
426
    }
427
428
    /**
429
     * @param string $transactionType
430
     * @param Carbon $endDate
431
     *
432
     * @return array
433
     */
434
    protected function getTransactionPeriodOverview(string $transactionType, Carbon $start, Carbon $end): array
435
    {
436
        $range = app('preferences')->get('viewRange', '1M')->data;
437
        $types = config(sprintf('firefly.transactionTypesByType.%s', $transactionType));
438
        [$start, $end] = $end < $start ? [$end, $start] : [$start, $end];
439
440
        // properties for cache
441
        $cache = new CacheProperties;
442
        $cache->addProperty($start);
443
        $cache->addProperty($end);
444
        $cache->addProperty('transactions-period-entries');
445
        $cache->addProperty($transactionType);
446
        if ($cache->has()) {
447
            return $cache->get(); // @codeCoverageIgnore
448
        }
449
        /** @var array $dates */
450
        $dates   = app('navigation')->blockPeriods($start, $end, $range);
451
        $entries = [];
452
453
        // collect all journals in this period (regardless of type)
454
        $collector = app(GroupCollectorInterface::class);
455
        $collector->setTypes($types)->setRange($start, $end);
456
        $genericSet = $collector->getExtractedJournals();
457
458
        foreach ($dates as $currentDate) {
459
            $spent       = [];
460
            $earned      = [];
461
            $transferred = [];
462
            $title       = app('navigation')->periodShow($currentDate['end'], $currentDate['period']);
463
464
            // set to correct array
465
            if ('expenses' === $transactionType || 'withdrawal' === $transactionType) {
466
                $spent = $this->filterJournalsByDate($genericSet, $currentDate['start'], $currentDate['end']);
467
            }
468
            if ('revenue' === $transactionType || 'deposit' === $transactionType) {
469
                $earned = $this->filterJournalsByDate($genericSet, $currentDate['start'], $currentDate['end']);
470
            }
471
            if ('transfer' === $transactionType || 'transfers' === $transactionType) {
472
                $transferred = $this->filterJournalsByDate($genericSet, $currentDate['start'], $currentDate['end']);
473
            }
474
475
476
            $entries[] =
477
                [
478
                    'title'              => $title,
479
                    'route'              =>
480
                        route('transactions.index', [$transactionType, $currentDate['start']->format('Y-m-d'), $currentDate['end']->format('Y-m-d')]),
481
                    'total_transactions' => count($spent) + count($earned) + count($transferred),
482
                    'spent'              => $this->groupByCurrency($spent),
483
                    'earned'             => $this->groupByCurrency($earned),
484
                    'transferred'        => $this->groupByCurrency($transferred),
485
                ];
486
        }
487
488
        return $entries;
489
    }
490
491
    /**
492
     * Return only transactions where $account is the source.
493
     * @param Account $account
494
     * @param array $journals
495
     * @return array
496
     */
497
    private function filterTransferredAway(Account $account, array $journals): array
498
    {
499
        $return = [];
500
        /** @var array $journal */
501
        foreach ($journals as $journal) {
502
            if ($account->id === (int)$journal['source_account_id']) {
503
                $return[] = $journal;
504
            }
505
        }
506
507
        return $return;
508
    }
509
510
    /**
511
     * Return only transactions where $account is the source.
512
     * @param Account $account
513
     * @param array $journals
514
     * @return array
515
     * @codeCoverageIgnore
516
     */
517
    private function filterTransferredIn(Account $account, array $journals): array
518
    {
519
        $return = [];
520
        /** @var array $journal */
521
        foreach ($journals as $journal) {
522
            if ($account->id === (int)$journal['destination_account_id']) {
523
                $return[] = $journal;
524
            }
525
        }
526
527
        return $return;
528
    }
529
530
    /**
531
     * Filter a list of journals by a set of dates, and then group them by currency.
532
     *
533
     * @param array $array
534
     * @param Carbon $start
535
     * @param Carbon $end
536
     * @return array
537
     */
538
    private function filterJournalsByDate(array $array, Carbon $start, Carbon $end): array
539
    {
540
        $result = [];
541
        /** @var array $journal */
542
        foreach ($array as $journal) {
543
            if ($journal['date'] <= $end && $journal['date'] >= $start) {
544
                $result[] = $journal;
545
            }
546
        }
547
548
        return $result;
549
    }
550
551
    /**
552
     * @param array $journals
553
     *
554
     * @return array
555
     * @codeCoverageIgnore
556
     */
557
    private function groupByCurrency(array $journals): array
558
    {
559
        $return = [];
560
        /** @var array $journal */
561
        foreach ($journals as $journal) {
562
            $currencyId        = (int)$journal['currency_id'];
563
            $foreignCurrencyId = $journal['foreign_currency_id'];
564
            if (!isset($return[$currencyId])) {
565
                $return[$currencyId] = [
566
                    'amount'                  => '0',
567
                    'count'                   => 0,
568
                    'currency_id'             => $currencyId,
569
                    'currency_name'           => $journal['currency_name'],
570
                    'currency_code'           => $journal['currency_code'],
571
                    'currency_symbol'         => $journal['currency_symbol'],
572
                    'currency_decimal_places' => $journal['currency_decimal_places'],
573
                ];
574
            }
575
            $return[$currencyId]['amount'] = bcadd($return[$currencyId]['amount'], $journal['amount'] ?? '0');
576
            $return[$currencyId]['count']++;
577
578
            if (null !== $foreignCurrencyId && null !== $journal['foreign_amount']) {
579
                if (!isset($return[$foreignCurrencyId])) {
580
                    $return[$foreignCurrencyId] = [
581
                        'amount'                  => '0',
582
                        'count'                   => 0,
583
                        'currency_id'             => (int)$foreignCurrencyId,
584
                        'currency_name'           => $journal['foreign_currency_name'],
585
                        'currency_code'           => $journal['foreign_currency_code'],
586
                        'currency_symbol'         => $journal['foreign_currency_symbol'],
587
                        'currency_decimal_places' => $journal['foreign_currency_decimal_places'],
588
                    ];
589
590
                }
591
                $return[$foreignCurrencyId]['count']++;
592
                $return[$foreignCurrencyId]['amount'] = bcadd($return[$foreignCurrencyId]['amount'], $journal['foreign_amount']);
593
            }
594
595
        }
596
597
        return $return;
598
    }
599
}
600