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 ( 37b02e...ebbbe1 )
by James
08:59
created

app/Repositories/Budget/BudgetRepository.php (2 issues)

Labels
Severity
1
<?php
2
/**
3
 * BudgetRepository.php
4
 * Copyright (c) 2017 [email protected]
5
 *
6
 * This file is part of Firefly III.
7
 *
8
 * Firefly III is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU General Public License as published by
10
 * the Free Software Foundation, either version 3 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * Firefly III 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 General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU General Public License
19
 * along with Firefly III. If not, see <http://www.gnu.org/licenses/>.
20
 */
21
declare(strict_types=1);
22
23
namespace FireflyIII\Repositories\Budget;
24
25
use Carbon\Carbon;
26
use FireflyIII\Helpers\Collector\JournalCollectorInterface;
27
use FireflyIII\Models\AccountType;
28
use FireflyIII\Models\AvailableBudget;
29
use FireflyIII\Models\Budget;
30
use FireflyIII\Models\BudgetLimit;
31
use FireflyIII\Models\Transaction;
32
use FireflyIII\Models\TransactionCurrency;
33
use FireflyIII\Models\TransactionJournal;
34
use FireflyIII\Models\TransactionType;
35
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
36
use FireflyIII\User;
37
use Illuminate\Database\Eloquent\Builder;
38
use Illuminate\Support\Collection;
39
use Log;
40
use Navigation;
41
use stdClass;
42
43
/**
44
 * Class BudgetRepository.
45
 */
46
class BudgetRepository implements BudgetRepositoryInterface
47
{
48
    /** @var User */
49
    private $user;
50
51
    /**
52
     * A method that returns the amount of money budgeted per day for this budget,
53
     * on average.
54
     *
55
     * @param Budget $budget
56
     *
57
     * @return string
58
     */
59
    public function budgetedPerDay(Budget $budget): string
60
    {
61
        $total = '0';
62
        $count = 0;
63
        foreach ($budget->budgetlimits as $limit) {
64
            $diff   = (string)$limit->start_date->diffInDays($limit->end_date);
65
            $amount = (string)$limit->amount;
66
            $perDay = bcdiv($amount, $diff);
67
            $total  = bcadd($total, $perDay);
68
            $count++;
69
        }
70
        $avg = $total;
71
        if ($count > 0) {
72
            $avg = bcdiv($total, (string)$count);
73
        }
74
75
        return $avg;
76
    }
77
78
    /**
79
     * @return bool
80
     *
81
82
83
     */
84
    public function cleanupBudgets(): bool
85
    {
86
        // delete limits with amount 0:
87
        BudgetLimit::where('amount', 0)->delete();
88
89
        // do the clean up by hand because Sqlite can be tricky with this.
90
        $budgetLimits = BudgetLimit::orderBy('created_at', 'DESC')->get(['id', 'budget_id', 'start_date', 'end_date']);
91
        $count        = [];
92
        /** @var BudgetLimit $budgetLimit */
93
        foreach ($budgetLimits as $budgetLimit) {
94
            $key = $budgetLimit->budget_id . '-' . $budgetLimit->start_date->format('Y-m-d') . $budgetLimit->end_date->format('Y-m-d');
95
            if (isset($count[$key])) {
96
                // delete it!
97
                BudgetLimit::find($budgetLimit->id)->delete();
98
            }
99
            $count[$key] = true;
100
        }
101
102
        return true;
103
    }
104
105
    /**
106
     * This method collects various info on budgets, used on the budget page and on the index.
107
     *
108
     * @param Collection $budgets
109
     * @param Carbon     $start
110
     * @param Carbon     $end
111
     *
112
     * @return array
113
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
114
     */
115
    public function collectBudgetInformation(Collection $budgets, Carbon $start, Carbon $end): array
116
    {
117
        // get account information
118
        /** @var AccountRepositoryInterface $accountRepository */
119
        $accountRepository = app(AccountRepositoryInterface::class);
120
        $accounts          = $accountRepository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]);
121
        $defaultCurrency   = app('amount')->getDefaultCurrency();
122
        $return            = [];
123
        /** @var Budget $budget */
124
        foreach ($budgets as $budget) {
125
            $budgetId          = $budget->id;
126
            $return[$budgetId] = [
127
                'spent'      => $this->spentInPeriod(new Collection([$budget]), $accounts, $start, $end),
128
                'budgeted'   => '0',
129
                'currentRep' => false,
130
            ];
131
            $budgetLimits      = $this->getBudgetLimits($budget, $start, $end);
132
            $otherLimits       = new Collection;
133
134
            // get all the budget limits relevant between start and end and examine them:
135
            /** @var BudgetLimit $limit */
136
            foreach ($budgetLimits as $limit) {
137
                if ($limit->start_date->isSameDay($start) && $limit->end_date->isSameDay($end)
138
                ) {
139
                    $return[$budgetId]['currentLimit'] = $limit;
140
                    $return[$budgetId]['budgeted']     = round($limit->amount, $defaultCurrency->decimal_places);
141
                    continue;
142
                }
143
                // otherwise it's just one of the many relevant repetitions:
144
                $otherLimits->push($limit);
145
            }
146
            $return[$budgetId]['otherLimits'] = $otherLimits;
147
        }
148
149
        return $return;
150
    }
151
152
    /**
153
     * @param Budget $budget
154
     *
155
     * @return bool
156
     *
157
158
     */
159
    public function destroy(Budget $budget): bool
160
    {
161
        $budget->delete();
162
163
        return true;
164
    }
165
166
    /**
167
     * Filters entries from the result set generated by getBudgetPeriodReport.
168
     *
169
     * @param Collection $set
170
     * @param int        $budgetId
171
     * @param array      $periods
172
     *
173
     * @return array
174
     */
175
    public function filterAmounts(Collection $set, int $budgetId, array $periods): array
176
    {
177
        $arr  = [];
178
        $keys = array_keys($periods);
179
        foreach ($keys as $period) {
180
            /** @var stdClass $object */
181
            $result = $set->filter(
182
                function (TransactionJournal $object) use ($budgetId, $period) {
183
                    $result = (string)$object->period_marker === (string)$period && $budgetId === (int)$object->budget_id;
0 ignored issues
show
The property budget_id does not exist on FireflyIII\Models\TransactionJournal. Did you mean budgets?
Loading history...
The property period_marker does not seem to exist on FireflyIII\Models\TransactionJournal. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
184
185
                    return $result;
186
                }
187
            );
188
            $amount = '0';
189
            if (null !== $result->first()) {
190
                $amount = $result->first()->sum_of_period;
191
            }
192
193
            $arr[$period] = $amount;
194
        }
195
196
        return $arr;
197
    }
198
199
    /**
200
     * Find a budget.
201
     *
202
     * @param int $budgetId
203
     *
204
     * @return Budget
205
     */
206
    public function find(int $budgetId): Budget
207
    {
208
        $budget = $this->user->budgets()->find($budgetId);
209
        if (null === $budget) {
210
            $budget = new Budget;
211
        }
212
213
        return $budget;
214
    }
215
216
    /**
217
     * Find a budget.
218
     *
219
     * @param string $name
220
     *
221
     * @return Budget|null
222
     */
223
    public function findByName(string $name): ?Budget
224
    {
225
        $budgets = $this->user->budgets()->get(['budgets.*']);
226
        /** @var Budget $budget */
227
        foreach ($budgets as $budget) {
228
            if ($budget->name === $name) {
229
                return $budget;
230
            }
231
        }
232
233
        return null;
234
    }
235
236
    /**
237
     * Find a budget or return NULL
238
     *
239
     * @param int $budgetId
240
     *
241
     * @return Budget|null
242
     */
243
    public function findNull(int $budgetId): ?Budget
244
    {
245
        return $this->user->budgets()->find($budgetId);
246
    }
247
248
    /**
249
     * This method returns the oldest journal or transaction date known to this budget.
250
     * Will cache result.
251
     *
252
     * @param Budget $budget
253
     *
254
     * @return Carbon
255
     */
256
    public function firstUseDate(Budget $budget): Carbon
257
    {
258
        $oldest  = Carbon::create()->startOfYear();
259
        $journal = $budget->transactionJournals()->orderBy('date', 'ASC')->first();
260
        if (null !== $journal) {
261
            $oldest = $journal->date < $oldest ? $journal->date : $oldest;
262
        }
263
264
        $transaction = $budget
265
            ->transactions()
266
            ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.id')
267
            ->orderBy('transaction_journals.date', 'ASC')->first(['transactions.*', 'transaction_journals.date']);
268
        if (null !== $transaction) {
269
            $carbon = new Carbon($transaction->date);
270
            $oldest = $carbon < $oldest ? $carbon : $oldest;
271
        }
272
273
        return $oldest;
274
    }
275
276
    /**
277
     * @return Collection
278
     */
279
    public function getActiveBudgets(): Collection
280
    {
281
        /** @var Collection $set */
282
        $set = $this->user->budgets()->where('active', 1)->get();
283
284
        $set = $set->sortBy(
285
            function (Budget $budget) {
286
                return strtolower($budget->name);
287
            }
288
        );
289
290
        return $set;
291
    }
292
293
    /**
294
     * @param Carbon $start
295
     * @param Carbon $end
296
     *
297
     * @return Collection
298
     */
299
    public function getAllBudgetLimits(Carbon $start, Carbon $end): Collection
300
    {
301
        $set = BudgetLimit::leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id')
302
                          ->with(['budget'])
303
                          ->where('budgets.user_id', $this->user->id)
304
                          ->where(
305
                              function (Builder $q5) use ($start, $end) {
306
                                  $q5->where(
307
                                      function (Builder $q1) use ($start, $end) {
308
                                          $q1->where(
309
                                              function (Builder $q2) use ($start, $end) {
310
                                                  $q2->where('budget_limits.end_date', '>=', $start->format('Y-m-d 00:00:00'));
311
                                                  $q2->where('budget_limits.end_date', '<=', $end->format('Y-m-d 00:00:00'));
312
                                              }
313
                                          )
314
                                             ->orWhere(
315
                                                 function (Builder $q3) use ($start, $end) {
316
                                                     $q3->where('budget_limits.start_date', '>=', $start->format('Y-m-d 00:00:00'));
317
                                                     $q3->where('budget_limits.start_date', '<=', $end->format('Y-m-d 00:00:00'));
318
                                                 }
319
                                             );
320
                                      }
321
                                  )
322
                                     ->orWhere(
323
                                         function (Builder $q4) use ($start, $end) {
324
                                             // or start is before start AND end is after end.
325
                                             $q4->where('budget_limits.start_date', '<=', $start->format('Y-m-d 00:00:00'));
326
                                             $q4->where('budget_limits.end_date', '>=', $end->format('Y-m-d 00:00:00'));
327
                                         }
328
                                     );
329
                              }
330
                          )->get(['budget_limits.*']);
331
332
        return $set;
333
    }
334
335
    /**
336
     * @param TransactionCurrency $currency
337
     * @param Carbon              $start
338
     * @param Carbon              $end
339
     *
340
     * @return string
341
     */
342
    public function getAvailableBudget(TransactionCurrency $currency, Carbon $start, Carbon $end): string
343
    {
344
        $amount          = '0';
345
        $availableBudget = $this->user->availableBudgets()
346
                                      ->where('transaction_currency_id', $currency->id)
347
                                      ->where('start_date', $start->format('Y-m-d 00:00:00'))
348
                                      ->where('end_date', $end->format('Y-m-d 00:00:00'))->first();
349
        if (null !== $availableBudget) {
350
            $amount = (string)$availableBudget->amount;
351
        }
352
353
        return $amount;
354
    }
355
356
    /**
357
     * @param Budget $budget
358
     * @param Carbon $start
359
     * @param Carbon $end
360
     *
361
     * @return Collection
362
     */
363
    public function getBudgetLimits(Budget $budget, Carbon $start, Carbon $end): Collection
364
    {
365
        $set = $budget->budgetlimits()
366
                      ->where(
367
                          function (Builder $q5) use ($start, $end) {
368
                              $q5->where(
369
                                  function (Builder $q1) use ($start, $end) {
370
                                      // budget limit ends within period
371
                                      $q1->where(
372
                                          function (Builder $q2) use ($start, $end) {
373
                                              $q2->where('budget_limits.end_date', '>=', $start->format('Y-m-d 00:00:00'));
374
                                              $q2->where('budget_limits.end_date', '<=', $end->format('Y-m-d 00:00:00'));
375
                                          }
376
                                      )
377
                                          // budget limit start within period
378
                                         ->orWhere(
379
                                              function (Builder $q3) use ($start, $end) {
380
                                                  $q3->where('budget_limits.start_date', '>=', $start->format('Y-m-d 00:00:00'));
381
                                                  $q3->where('budget_limits.start_date', '<=', $end->format('Y-m-d 00:00:00'));
382
                                              }
383
                                          );
384
                                  }
385
                              )
386
                                 ->orWhere(
387
                                     function (Builder $q4) use ($start, $end) {
388
                                         // or start is before start AND end is after end.
389
                                         $q4->where('budget_limits.start_date', '<=', $start->format('Y-m-d 00:00:00'));
390
                                         $q4->where('budget_limits.end_date', '>=', $end->format('Y-m-d 00:00:00'));
391
                                     }
392
                                 );
393
                          }
394
                      )->orderBy('budget_limits.start_date', 'DESC')->get(['budget_limits.*']);
395
396
        return $set;
397
    }
398
399
    /**
400
     * This method is being used to generate the budget overview in the year/multi-year report. Its used
401
     * in both the year/multi-year budget overview AND in the accompanying chart.
402
     *
403
     * @param Collection $budgets
404
     * @param Collection $accounts
405
     * @param Carbon     $start
406
     * @param Carbon     $end
407
     *
408
     * @return array
409
     */
410
    public function getBudgetPeriodReport(Collection $budgets, Collection $accounts, Carbon $start, Carbon $end): array
411
    {
412
        $carbonFormat = Navigation::preferredCarbonFormat($start, $end);
413
        $data         = [];
414
        // prep data array:
415
        /** @var Budget $budget */
416
        foreach ($budgets as $budget) {
417
            $data[$budget->id] = [
418
                'name'    => $budget->name,
419
                'sum'     => '0',
420
                'entries' => [],
421
            ];
422
        }
423
424
        // get all transactions:
425
        /** @var JournalCollectorInterface $collector */
426
        $collector = app(JournalCollectorInterface::class);
427
        $collector->setAccounts($accounts)->setRange($start, $end);
428
        $collector->setBudgets($budgets);
429
        $transactions = $collector->getJournals();
430
431
        // loop transactions:
432
        /** @var Transaction $transaction */
433
        foreach ($transactions as $transaction) {
434
            $budgetId                          = max((int)$transaction->transaction_journal_budget_id, (int)$transaction->transaction_budget_id);
435
            $date                              = $transaction->date->format($carbonFormat);
436
            $data[$budgetId]['entries'][$date] = bcadd($data[$budgetId]['entries'][$date] ?? '0', $transaction->transaction_amount);
437
        }
438
439
        return $data;
440
    }
441
442
    /**
443
     * @return Collection
444
     */
445
    public function getBudgets(): Collection
446
    {
447
        /** @var Collection $set */
448
        $set = $this->user->budgets()->get();
449
450
        $set = $set->sortBy(
451
            function (Budget $budget) {
452
                return strtolower($budget->name);
453
            }
454
        );
455
456
        return $set;
457
    }
458
459
    /**
460
     * @return Collection
461
     */
462
    public function getInactiveBudgets(): Collection
463
    {
464
        /** @var Collection $set */
465
        $set = $this->user->budgets()->where('active', 0)->get();
466
467
        $set = $set->sortBy(
468
            function (Budget $budget) {
469
                return strtolower($budget->name);
470
            }
471
        );
472
473
        return $set;
474
    }
475
476
    /**
477
     * @param Collection $accounts
478
     * @param Carbon     $start
479
     * @param Carbon     $end
480
     *
481
     * @return array
482
     */
483
    public function getNoBudgetPeriodReport(Collection $accounts, Carbon $start, Carbon $end): array
484
    {
485
        $carbonFormat = Navigation::preferredCarbonFormat($start, $end);
486
        /** @var JournalCollectorInterface $collector */
487
        $collector = app(JournalCollectorInterface::class);
488
        $collector->setAccounts($accounts)->setRange($start, $end);
489
        $collector->setTypes([TransactionType::WITHDRAWAL]);
490
        $collector->withoutBudget();
491
        $transactions = $collector->getJournals();
492
        $result       = [
493
            'entries' => [],
494
            'name'    => (string)trans('firefly.no_budget'),
495
            'sum'     => '0',
496
        ];
497
498
        foreach ($transactions as $transaction) {
499
            $date = $transaction->date->format($carbonFormat);
500
501
            if (!isset($result['entries'][$date])) {
502
                $result['entries'][$date] = '0';
503
            }
504
            $result['entries'][$date] = bcadd($result['entries'][$date], $transaction->transaction_amount);
505
        }
506
507
        return $result;
508
    }
509
510
    /**
511
     * @param TransactionCurrency $currency
512
     * @param Carbon              $start
513
     * @param Carbon              $end
514
     * @param string              $amount
515
     *
516
     * @return bool
517
     */
518
    public function setAvailableBudget(TransactionCurrency $currency, Carbon $start, Carbon $end, string $amount): bool
519
    {
520
        $availableBudget = $this->user->availableBudgets()
521
                                      ->where('transaction_currency_id', $currency->id)
522
                                      ->where('start_date', $start->format('Y-m-d 00:00:00'))
523
                                      ->where('end_date', $end->format('Y-m-d 00:00:00'))->first();
524
        if (null === $availableBudget) {
525
            $availableBudget = new AvailableBudget;
526
            $availableBudget->user()->associate($this->user);
527
            $availableBudget->transactionCurrency()->associate($currency);
528
            $availableBudget->start_date = $start->format('Y-m-d 00:00:00');
529
            $availableBudget->end_date   = $end->format('Y-m-d 00:00:00');
530
        }
531
        $availableBudget->amount = $amount;
532
        $availableBudget->save();
533
534
        return true;
535
    }
536
537
    /**
538
     * @param User $user
539
     */
540
    public function setUser(User $user)
541
    {
542
        $this->user = $user;
543
    }
544
545
    /**
546
     * @param Collection $budgets
547
     * @param Collection $accounts
548
     * @param Carbon     $start
549
     * @param Carbon     $end
550
     *
551
     * @return string
552
     */
553
    public function spentInPeriod(Collection $budgets, Collection $accounts, Carbon $start, Carbon $end): string
554
    {
555
        /** @var JournalCollectorInterface $collector */
556
        $collector = app(JournalCollectorInterface::class);
557
        $collector->setUser($this->user);
558
        $collector->setRange($start, $end)->setBudgets($budgets)->withBudgetInformation();
559
560
        if ($accounts->count() > 0) {
561
            $collector->setAccounts($accounts);
562
        }
563
        if (0 === $accounts->count()) {
564
            $collector->setAllAssetAccounts();
565
        }
566
567
        $set = $collector->getJournals();
568
569
        return strval($set->sum('transaction_amount'));
570
    }
571
572
    /**
573
     * @param Collection $accounts
574
     * @param Carbon     $start
575
     * @param Carbon     $end
576
     *
577
     * @return string
578
     */
579
    public function spentInPeriodWoBudget(Collection $accounts, Carbon $start, Carbon $end): string
580
    {
581
        /** @var JournalCollectorInterface $collector */
582
        $collector = app(JournalCollectorInterface::class);
583
        $collector->setUser($this->user);
584
        $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->withoutBudget();
585
586
        if ($accounts->count() > 0) {
587
            $collector->setAccounts($accounts);
588
        }
589
        if (0 === $accounts->count()) {
590
            $collector->setAllAssetAccounts();
591
        }
592
593
        $set = $collector->getJournals();
594
        $set = $set->filter(
595
            function (Transaction $transaction) {
596
                if (bccomp($transaction->transaction_amount, '0') === -1) {
597
                    return $transaction;
598
                }
599
600
                return null;
601
            }
602
        );
603
604
        return strval($set->sum('transaction_amount'));
605
    }
606
607
    /**
608
     * @param array $data
609
     *
610
     * @return Budget
611
     */
612
    public function store(array $data): Budget
613
    {
614
        $newBudget = new Budget(
615
            [
616
                'user_id' => $this->user->id,
617
                'name'    => $data['name'],
618
            ]
619
        );
620
        $newBudget->save();
621
622
        return $newBudget;
623
    }
624
625
    /**
626
     * @param Budget $budget
627
     * @param array  $data
628
     *
629
     * @return Budget
630
     */
631
    public function update(Budget $budget, array $data): Budget
632
    {
633
        // update the account:
634
        $budget->name   = $data['name'];
635
        $budget->active = $data['active'];
636
        $budget->save();
637
638
        return $budget;
639
    }
640
641
    /**
642
     * @param Budget $budget
643
     * @param Carbon $start
644
     * @param Carbon $end
645
     * @param string $amount
646
     *
647
     * @return BudgetLimit
648
     *
649
650
     */
651
    public function updateLimitAmount(Budget $budget, Carbon $start, Carbon $end, string $amount): BudgetLimit
652
    {
653
        $this->cleanupBudgets();
654
        // count the limits:
655
        $limits = $budget->budgetlimits()
656
                         ->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00'))
657
                         ->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00'))
658
                         ->get(['budget_limits.*'])->count();
659
        Log::debug(sprintf('Found %d budget limits.', $limits));
660
        // there might be a budget limit for these dates:
661
        /** @var BudgetLimit $limit */
662
        $limit = $budget->budgetlimits()
663
                        ->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00'))
664
                        ->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00'))
665
                        ->first(['budget_limits.*']);
666
667
        // if more than 1 limit found, delete the others:
668
        if ($limits > 1 && null !== $limit) {
669
            Log::debug(sprintf('Found more than 1, delete all except #%d', $limit->id));
670
            $budget->budgetlimits()
671
                   ->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00'))
672
                   ->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00'))
673
                   ->where('budget_limits.id', '!=', $limit->id)->delete();
674
        }
675
676
        // delete if amount is zero.
677
        // Returns 0 if the two operands are equal,
678
        // 1 if the left_operand is larger than the right_operand, -1 otherwise.
679
        if (null !== $limit && bccomp($amount, '0') <= 0) {
680
            Log::debug(sprintf('%s is zero, delete budget limit #%d', $amount, $limit->id));
681
            $limit->delete();
682
683
            return new BudgetLimit;
684
        }
685
        // update if exists:
686
        if (null !== $limit) {
687
            Log::debug(sprintf('Existing budget limit is #%d, update this to amount %s', $limit->id, $amount));
688
            $limit->amount = $amount;
689
            $limit->save();
690
691
            return $limit;
692
        }
693
        Log::debug('No existing budget limit, create a new one');
694
        // or create one and return it.
695
        $limit = new BudgetLimit;
696
        $limit->budget()->associate($budget);
697
        $limit->start_date = $start->format('Y-m-d 00:00:00');
698
        $limit->end_date   = $end->format('Y-m-d 00:00:00');
699
        $limit->amount     = $amount;
700
        $limit->save();
701
        Log::debug(sprintf('Created new budget limit with ID #%d and amount %s', $limit->id, $amount));
702
703
        return $limit;
704
    }
705
}
706