Completed
Push — master ( bae40e...a4cb2c )
by James
19:36 queued 09:47
created

BudgetRepository::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 0
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 Exception;
27
use FireflyIII\Exceptions\FireflyException;
28
use FireflyIII\Helpers\Collector\TransactionCollectorInterface;
29
use FireflyIII\Models\AccountType;
30
use FireflyIII\Models\AvailableBudget;
31
use FireflyIII\Models\Budget;
32
use FireflyIII\Models\BudgetLimit;
33
use FireflyIII\Models\RuleAction;
34
use FireflyIII\Models\RuleTrigger;
35
use FireflyIII\Models\Transaction;
36
use FireflyIII\Models\TransactionCurrency;
37
use FireflyIII\Models\TransactionType;
38
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
39
use FireflyIII\User;
40
use Illuminate\Database\Eloquent\Builder;
41
use Illuminate\Support\Collection;
42
use Log;
43
use Navigation;
44
45
/**
46
 * Class BudgetRepository.
47
 *
48
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
49
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
50
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
51
 */
52
class BudgetRepository implements BudgetRepositoryInterface
53
{
54
    /** @var User */
55
    private $user;
56
57
    /**
58
     * Constructor.
59
     */
60
    public function __construct()
61
    {
62
        if ('testing' === env('APP_ENV')) {
63
            Log::warning(sprintf('%s should not be instantiated in the TEST environment!', \get_class($this)));
64
        }
65
    }
66
67
    /**
68
     * A method that returns the amount of money budgeted per day for this budget,
69
     * on average.
70
     *
71
     * @param Budget $budget
72
     *
73
     * @return string
74
     */
75
    public function budgetedPerDay(Budget $budget): string
76
    {
77
        Log::debug(sprintf('Now with budget #%d "%s"', $budget->id, $budget->name));
78
        $total = '0';
79
        $count = 0;
80
        foreach ($budget->budgetlimits as $limit) {
81
            $diff   = $limit->start_date->diffInDays($limit->end_date);
82
            $diff   = 0 === $diff ? 1 : $diff;
83
            $amount = (string)$limit->amount;
84
            $perDay = bcdiv($amount, (string)$diff);
85
            $total  = bcadd($total, $perDay);
86
            $count++;
87
            Log::debug(sprintf('Found %d budget limits. Per day is %s, total is %s', $count, $perDay, $total));
88
        }
89
        $avg = $total;
90
        if ($count > 0) {
91
            $avg = bcdiv($total, (string)$count);
92
        }
93
        Log::debug(sprintf('%s / %d = %s = average.', $total, $count, $avg));
94
95
        return $avg;
96
    }
97
98
    /**
99
     * @return bool
100
     * @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's 5.
101
     */
102
    public function cleanupBudgets(): bool
103
    {
104
        // delete limits with amount 0:
105
        try {
106
            BudgetLimit::where('amount', 0)->delete();
107
        } catch (Exception $e) {
108
            Log::debug(sprintf('Could not delete budget limit: %s', $e->getMessage()));
109
        }
110
111
        // do the clean up by hand because Sqlite can be tricky with this.
112
        $budgetLimits = BudgetLimit::orderBy('created_at', 'DESC')->get(['id', 'budget_id', 'start_date', 'end_date']);
113
        $count        = [];
114
        /** @var BudgetLimit $budgetLimit */
115
        foreach ($budgetLimits as $budgetLimit) {
116
            $key = $budgetLimit->budget_id . '-' . $budgetLimit->start_date->format('Y-m-d') . $budgetLimit->end_date->format('Y-m-d');
117
            if (isset($count[$key])) {
118
                // delete it!
119
                try {
120
                    BudgetLimit::find($budgetLimit->id)->delete();
121
                } catch (Exception $e) {
122
                    Log::debug(sprintf('Could not delete budget limit: %s', $e->getMessage()));
123
                }
124
            }
125
            $count[$key] = true;
126
        }
127
128
        return true;
129
    }
130
131
    /**
132
     * This method collects various info on budgets, used on the budget page and on the index.
133
     *
134
     * @param Collection $budgets
135
     * @param Carbon     $start
136
     * @param Carbon     $end
137
     *
138
     * @return array
139
     *
140
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
141
     */
142
    public function collectBudgetInformation(Collection $budgets, Carbon $start, Carbon $end): array
143
    {
144
        // get account information
145
        /** @var AccountRepositoryInterface $accountRepository */
146
        $accountRepository = app(AccountRepositoryInterface::class);
147
        $accounts          = $accountRepository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]);
148
        $defaultCurrency   = app('amount')->getDefaultCurrency();
149
        $return            = [];
150
        /** @var Budget $budget */
151
        foreach ($budgets as $budget) {
152
            $budgetId          = $budget->id;
153
            $return[$budgetId] = [
154
                'spent'    => $this->spentInPeriod(new Collection([$budget]), $accounts, $start, $end),
155
                'budgeted' => '0',
156
            ];
157
            $budgetLimits      = $this->getBudgetLimits($budget, $start, $end);
158
            $otherLimits       = new Collection;
159
160
            // get all the budget limits relevant between start and end and examine them:
161
            /** @var BudgetLimit $limit */
162
            foreach ($budgetLimits as $limit) {
163
                if ($limit->start_date->isSameDay($start) && $limit->end_date->isSameDay($end)
164
                ) {
165
                    $return[$budgetId]['currentLimit'] = $limit;
166
                    $return[$budgetId]['budgeted']     = round($limit->amount, $defaultCurrency->decimal_places);
167
                    continue;
168
                }
169
                // otherwise it's just one of the many relevant repetitions:
170
                $otherLimits->push($limit);
171
            }
172
            $return[$budgetId]['otherLimits'] = $otherLimits;
173
        }
174
175
        return $return;
176
    }
177
178
    /**
179
     * @param Budget $budget
180
     *
181
     * @return bool
182
     */
183
    public function destroy(Budget $budget): bool
184
    {
185
        try {
186
            $budget->delete();
187
        } catch (Exception $e) {
188
            Log::error(sprintf('Could not delete budget: %s', $e->getMessage()));
189
        }
190
191
        return true;
192
    }
193
194
    /**
195
     * @param AvailableBudget $availableBudget
196
     */
197
    public function destroyAvailableBudget(AvailableBudget $availableBudget): void
198
    {
199
        try {
200
            $availableBudget->delete();
201
        } catch (Exception $e) {
202
            Log::error(sprintf('Could not delete available budget: %s', $e->getMessage()));
203
        }
204
    }
205
206
    /**
207
     * Destroy a budget limit.
208
     *
209
     * @param BudgetLimit $budgetLimit
210
     */
211
    public function destroyBudgetLimit(BudgetLimit $budgetLimit): void
212
    {
213
        try {
214
            $budgetLimit->delete();
215
        } catch (Exception $e) {
216
            Log::info(sprintf('Could not delete budget limit: %s', $e->getMessage()));
217
        }
218
    }
219
220
    /**
221
     * Find a budget.
222
     *
223
     * @param string $name
224
     *
225
     * @return Budget|null
226
     */
227
    public function findByName(string $name): ?Budget
228
    {
229
        $budgets = $this->user->budgets()->get(['budgets.*']);
230
        /** @var Budget $budget */
231
        foreach ($budgets as $budget) {
232
            if ($budget->name === $name) {
233
                return $budget;
234
            }
235
        }
236
237
        return null;
238
    }
239
240
    /**
241
     * Find a budget or return NULL
242
     *
243
     * @param int $budgetId |null
244
     *
245
     * @return Budget|null
246
     */
247
    public function findNull(int $budgetId = null): ?Budget
248
    {
249
        if (null === $budgetId) {
250
            return null;
251
        }
252
253
        return $this->user->budgets()->find($budgetId);
254
    }
255
256
    /**
257
     * This method returns the oldest journal or transaction date known to this budget.
258
     * Will cache result.
259
     *
260
     * @param Budget $budget
261
     *
262
     * @return Carbon
263
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
264
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
265
     */
266
    public function firstUseDate(Budget $budget): ?Carbon
267
    {
268
        $oldest  = null;
269
        $journal = $budget->transactionJournals()->orderBy('date', 'ASC')->first();
270
        if (null !== $journal) {
271
            $oldest = $journal->date < $oldest ? $journal->date : $oldest;
272
        }
273
274
        $transaction = $budget
275
            ->transactions()
276
            ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.id')
277
            ->orderBy('transaction_journals.date', 'ASC')->first(['transactions.*', 'transaction_journals.date']);
278
        if (null !== $transaction) {
279
            $carbon = new Carbon($transaction->date);
280
            $oldest = $carbon < $oldest ? $carbon : $oldest;
281
        }
282
283
        return $oldest;
284
    }
285
286
    /**
287
     * @return Collection
288
     */
289
    public function getActiveBudgets(): Collection
290
    {
291
        /** @var Collection $set */
292
        $set = $this->user->budgets()->where('active', 1)->get();
293
294
        $set = $set->sortBy(
295
            function (Budget $budget) {
296
                return strtolower($budget->name);
297
            }
298
        );
299
300
        return $set;
301
    }
302
303
    /**
304
     * @param Carbon $start
305
     * @param Carbon $end
306
     *
307
     * @return Collection
308
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
309
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
310
     */
311
    public function getAllBudgetLimits(Carbon $start = null, Carbon $end = null): Collection
312
    {
313
        // both are NULL:
314
        if (null === $start && null === $end) {
315
            $set = BudgetLimit::leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id')
316
                              ->with(['budget'])
317
                              ->where('budgets.user_id', $this->user->id)
318
                              ->get(['budget_limits.*']);
319
320
            return $set;
321
        }
322
        // one of the two is NULL.
323
        if (null === $start xor null === $end) {
324
            $query = BudgetLimit::leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id')
325
                                ->with(['budget'])
326
                                ->where('budgets.user_id', $this->user->id);
327
            if (null !== $end) {
328
                // end date must be before $end.
329
                $query->where('end_date', '<=', $end->format('Y-m-d 00:00:00'));
330
            }
331
            if (null !== $start) {
332
                // start date must be after $start.
333
                $query->where('start_date', '>=', $start->format('Y-m-d 00:00:00'));
334
            }
335
            $set = $query->get(['budget_limits.*']);
336
337
            return $set;
338
        }
339
        // neither are NULL:
340
        $set = BudgetLimit::leftJoin('budgets', 'budgets.id', '=', 'budget_limits.budget_id')
341
                          ->with(['budget'])
342
                          ->where('budgets.user_id', $this->user->id)
343
                          ->where(
344
                              function (Builder $q5) use ($start, $end) {
345
                                  $q5->where(
346
                                      function (Builder $q1) use ($start, $end) {
347
                                          $q1->where(
348
                                              function (Builder $q2) use ($start, $end) {
349
                                                  $q2->where('budget_limits.end_date', '>=', $start->format('Y-m-d 00:00:00'));
0 ignored issues
show
Bug introduced by James Cole
The method format() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

349
                                                  $q2->where('budget_limits.end_date', '>=', $start->/** @scrutinizer ignore-call */ format('Y-m-d 00:00:00'));

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
350
                                                  $q2->where('budget_limits.end_date', '<=', $end->format('Y-m-d 00:00:00'));
0 ignored issues
show
Bug introduced by James Cole
The method format() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

350
                                                  $q2->where('budget_limits.end_date', '<=', $end->/** @scrutinizer ignore-call */ format('Y-m-d 00:00:00'));

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
351
                                              }
352
                                          )
353
                                             ->orWhere(
354
                                                 function (Builder $q3) use ($start, $end) {
355
                                                     $q3->where('budget_limits.start_date', '>=', $start->format('Y-m-d 00:00:00'));
356
                                                     $q3->where('budget_limits.start_date', '<=', $end->format('Y-m-d 00:00:00'));
357
                                                 }
358
                                             );
359
                                      }
360
                                  )
361
                                     ->orWhere(
362
                                         function (Builder $q4) use ($start, $end) {
363
                                             // or start is before start AND end is after end.
364
                                             $q4->where('budget_limits.start_date', '<=', $start->format('Y-m-d 00:00:00'));
365
                                             $q4->where('budget_limits.end_date', '>=', $end->format('Y-m-d 00:00:00'));
366
                                         }
367
                                     );
368
                              }
369
                          )->get(['budget_limits.*']);
370
371
        return $set;
372
    }
373
374
    /**
375
     * @param TransactionCurrency $currency
376
     * @param Carbon              $start
377
     * @param Carbon              $end
378
     *
379
     * @return string
380
     */
381
    public function getAvailableBudget(TransactionCurrency $currency, Carbon $start, Carbon $end): string
382
    {
383
        $amount          = '0';
384
        $availableBudget = $this->user->availableBudgets()
385
                                      ->where('transaction_currency_id', $currency->id)
386
                                      ->where('start_date', $start->format('Y-m-d 00:00:00'))
387
                                      ->where('end_date', $end->format('Y-m-d 00:00:00'))->first();
388
        if (null !== $availableBudget) {
389
            $amount = (string)$availableBudget->amount;
390
        }
391
392
        return $amount;
393
    }
394
395
    /**
396
     * Returns all available budget objects.
397
     *
398
     * @return Collection
399
     */
400
    public function getAvailableBudgets(): Collection
401
    {
402
        return $this->user->availableBudgets()->get();
403
    }
404
405
    /**
406
     * Calculate the average amount in the budgets available in this period.
407
     * Grouped by day.
408
     *
409
     * @param Carbon $start
410
     * @param Carbon $end
411
     *
412
     * @return string
413
     */
414
    public function getAverageAvailable(Carbon $start, Carbon $end): string
415
    {
416
        /** @var Collection $list */
417
        $list = $this->user->availableBudgets()
418
                           ->where('start_date', '>=', $start->format('Y-m-d 00:00:00'))
419
                           ->where('end_date', '<=', $end->format('Y-m-d 00:00:00'))
420
                           ->get();
421
        if (0 === $list->count()) {
422
            return '0';
423
        }
424
        $total = '0';
425
        $days  = 0;
426
        /** @var AvailableBudget $availableBudget */
427
        foreach ($list as $availableBudget) {
428
            $total = bcadd($availableBudget->amount, $total);
429
            $days  += $availableBudget->start_date->diffInDays($availableBudget->end_date);
430
        }
431
432
        return bcdiv($total, (string)$days);
433
    }
434
435
    /**
436
     * @param Budget $budget
437
     * @param Carbon $start
438
     * @param Carbon $end
439
     *
440
     * @return Collection
441
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
442
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
443
     */
444
    public function getBudgetLimits(Budget $budget, Carbon $start = null, Carbon $end = null): Collection
445
    {
446
        if (null === $end && null === $start) {
447
            return $budget->budgetlimits()->orderBy('budget_limits.start_date', 'DESC')->get(['budget_limits.*']);
448
        }
449
        if (null === $end xor null === $start) {
450
            $query = $budget->budgetlimits()->orderBy('budget_limits.start_date', 'DESC');
451
            // one of the two is null
452
            if (null !== $end) {
453
                // end date must be before $end.
454
                $query->where('end_date', '<=', $end->format('Y-m-d 00:00:00'));
455
            }
456
            if (null !== $start) {
457
                // start date must be after $start.
458
                $query->where('start_date', '>=', $start->format('Y-m-d 00:00:00'));
459
            }
460
            $set = $query->get(['budget_limits.*']);
461
462
            return $set;
463
        }
464
465
        // when both dates are set:
466
        $set = $budget->budgetlimits()
467
                      ->where(
468
                          function (Builder $q5) use ($start, $end) {
469
                              $q5->where(
470
                                  function (Builder $q1) use ($start, $end) {
471
                                      // budget limit ends within period
472
                                      $q1->where(
473
                                          function (Builder $q2) use ($start, $end) {
474
                                              $q2->where('budget_limits.end_date', '>=', $start->format('Y-m-d 00:00:00'));
475
                                              $q2->where('budget_limits.end_date', '<=', $end->format('Y-m-d 00:00:00'));
476
                                          }
477
                                      )
478
                                          // budget limit start within period
479
                                         ->orWhere(
480
                                              function (Builder $q3) use ($start, $end) {
481
                                                  $q3->where('budget_limits.start_date', '>=', $start->format('Y-m-d 00:00:00'));
482
                                                  $q3->where('budget_limits.start_date', '<=', $end->format('Y-m-d 00:00:00'));
483
                                              }
484
                                          );
485
                                  }
486
                              )
487
                                 ->orWhere(
488
                                     function (Builder $q4) use ($start, $end) {
489
                                         // or start is before start AND end is after end.
490
                                         $q4->where('budget_limits.start_date', '<=', $start->format('Y-m-d 00:00:00'));
491
                                         $q4->where('budget_limits.end_date', '>=', $end->format('Y-m-d 00:00:00'));
492
                                     }
493
                                 );
494
                          }
495
                      )->orderBy('budget_limits.start_date', 'DESC')->get(['budget_limits.*']);
496
497
        return $set;
498
    }
499
500
    /** @noinspection MoreThanThreeArgumentsInspection */
501
    /**
502
     * This method is being used to generate the budget overview in the year/multi-year report. Its used
503
     * in both the year/multi-year budget overview AND in the accompanying chart.
504
     *
505
     * @param Collection $budgets
506
     * @param Collection $accounts
507
     * @param Carbon     $start
508
     * @param Carbon     $end
509
     *
510
     * @return array
511
     */
512
    public function getBudgetPeriodReport(Collection $budgets, Collection $accounts, Carbon $start, Carbon $end): array
513
    {
514
        $carbonFormat = Navigation::preferredCarbonFormat($start, $end);
0 ignored issues
show
Bug Best Practice introduced by James Cole
The method FireflyIII\Support\Facad...preferredCarbonFormat() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

514
        /** @scrutinizer ignore-call */ 
515
        $carbonFormat = Navigation::preferredCarbonFormat($start, $end);
Loading history...
515
        $data         = [];
516
        // prep data array:
517
        /** @var Budget $budget */
518
        foreach ($budgets as $budget) {
519
            $data[$budget->id] = [
520
                'name'    => $budget->name,
521
                'sum'     => '0',
522
                'entries' => [],
523
            ];
524
        }
525
526
        // get all transactions:
527
        /** @var TransactionCollectorInterface $collector */
528
        $collector = app(TransactionCollectorInterface::class);
529
        $collector->setAccounts($accounts)->setRange($start, $end);
530
        $collector->setBudgets($budgets);
531
        $transactions = $collector->getTransactions();
532
533
        // loop transactions:
534
        /** @var Transaction $transaction */
535
        foreach ($transactions as $transaction) {
536
            $budgetId                          = max((int)$transaction->transaction_journal_budget_id, (int)$transaction->transaction_budget_id);
537
            $date                              = $transaction->date->format($carbonFormat);
538
            $data[$budgetId]['entries'][$date] = bcadd($data[$budgetId]['entries'][$date] ?? '0', $transaction->transaction_amount);
539
        }
540
541
        return $data;
542
    }
543
544
    /**
545
     * @return Collection
546
     */
547
    public function getBudgets(): Collection
548
    {
549
        /** @var Collection $set */
550
        $set = $this->user->budgets()->get();
551
552
        $set = $set->sortBy(
553
            function (Budget $budget) {
554
                return strtolower($budget->name);
555
            }
556
        );
557
558
        return $set;
559
    }
560
561
    /**
562
     * Get all budgets with these ID's.
563
     *
564
     * @param array $budgetIds
565
     *
566
     * @return Collection
567
     */
568
    public function getByIds(array $budgetIds): Collection
569
    {
570
        return $this->user->budgets()->whereIn('id', $budgetIds)->get();
571
    }
572
573
    /**
574
     * @return Collection
575
     */
576
    public function getInactiveBudgets(): Collection
577
    {
578
        /** @var Collection $set */
579
        $set = $this->user->budgets()->where('active', 0)->get();
580
581
        $set = $set->sortBy(
582
            function (Budget $budget) {
583
                return strtolower($budget->name);
584
            }
585
        );
586
587
        return $set;
588
    }
589
590
    /**
591
     * @param Collection $accounts
592
     * @param Carbon     $start
593
     * @param Carbon     $end
594
     *
595
     * @return array
596
     */
597
    public function getNoBudgetPeriodReport(Collection $accounts, Carbon $start, Carbon $end): array
598
    {
599
        $carbonFormat = Navigation::preferredCarbonFormat($start, $end);
0 ignored issues
show
Bug Best Practice introduced by James Cole
The method FireflyIII\Support\Facad...preferredCarbonFormat() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

599
        /** @scrutinizer ignore-call */ 
600
        $carbonFormat = Navigation::preferredCarbonFormat($start, $end);
Loading history...
600
        /** @var TransactionCollectorInterface $collector */
601
        $collector = app(TransactionCollectorInterface::class);
602
        $collector->setAccounts($accounts)->setRange($start, $end);
603
        $collector->setTypes([TransactionType::WITHDRAWAL]);
604
        $collector->withoutBudget();
605
        $transactions = $collector->getTransactions();
606
        $result       = [
607
            'entries' => [],
608
            'name'    => (string)trans('firefly.no_budget'),
609
            'sum'     => '0',
610
        ];
611
612
        foreach ($transactions as $transaction) {
613
            $date = $transaction->date->format($carbonFormat);
614
615
            if (!isset($result['entries'][$date])) {
616
                $result['entries'][$date] = '0';
617
            }
618
            $result['entries'][$date] = bcadd($result['entries'][$date], $transaction->transaction_amount);
619
        }
620
621
        return $result;
622
    }
623
624
    /** @noinspection MoreThanThreeArgumentsInspection */
625
    /**
626
     * @param TransactionCurrency $currency
627
     * @param Carbon              $start
628
     * @param Carbon              $end
629
     * @param string              $amount
630
     *
631
     * @return AvailableBudget
632
     */
633
    public function setAvailableBudget(TransactionCurrency $currency, Carbon $start, Carbon $end, string $amount): AvailableBudget
634
    {
635
        $availableBudget = $this->user->availableBudgets()
636
                                      ->where('transaction_currency_id', $currency->id)
637
                                      ->where('start_date', $start->format('Y-m-d 00:00:00'))
638
                                      ->where('end_date', $end->format('Y-m-d 00:00:00'))->first();
639
        if (null === $availableBudget) {
640
            $availableBudget = new AvailableBudget;
641
            $availableBudget->user()->associate($this->user);
642
            $availableBudget->transactionCurrency()->associate($currency);
643
            $availableBudget->start_date = $start->format('Y-m-d 00:00:00');
644
            $availableBudget->end_date   = $end->format('Y-m-d 00:00:00');
645
        }
646
        $availableBudget->amount = $amount;
647
        $availableBudget->save();
648
649
        return $availableBudget;
650
    }
651
652
    /**
653
     * @param User $user
654
     */
655
    public function setUser(User $user): void
656
    {
657
        $this->user = $user;
658
    }
659
660
    /** @noinspection MoreThanThreeArgumentsInspection */
661
    /**
662
     * @param Collection $budgets
663
     * @param Collection $accounts
664
     * @param Carbon     $start
665
     * @param Carbon     $end
666
     *
667
     * @return string
668
     */
669
    public function spentInPeriod(Collection $budgets, Collection $accounts, Carbon $start, Carbon $end): string
670
    {
671
        /** @var TransactionCollectorInterface $collector */
672
        $collector = app(TransactionCollectorInterface::class);
673
        $collector->setUser($this->user);
674
        $collector->setRange($start, $end)->setBudgets($budgets)->withBudgetInformation();
675
676
        if ($accounts->count() > 0) {
677
            $collector->setAccounts($accounts);
678
        }
679
        if (0 === $accounts->count()) {
680
            $collector->setAllAssetAccounts();
681
        }
682
683
        $set = $collector->getTransactions();
684
685
        return (string)$set->sum('transaction_amount');
686
    }
687
688
    /**
689
     * @param Collection $accounts
690
     * @param Carbon     $start
691
     * @param Carbon     $end
692
     *
693
     * @return string
694
     */
695
    public function spentInPeriodWoBudget(Collection $accounts, Carbon $start, Carbon $end): string
696
    {
697
        /** @var TransactionCollectorInterface $collector */
698
        $collector = app(TransactionCollectorInterface::class);
699
        $collector->setUser($this->user);
700
        $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->withoutBudget();
701
702
        if ($accounts->count() > 0) {
703
            $collector->setAccounts($accounts);
704
        }
705
        if (0 === $accounts->count()) {
706
            $collector->setAllAssetAccounts();
707
        }
708
709
        $set = $collector->getTransactions();
710
        $set = $set->filter(
711
            function (Transaction $transaction) {
712
                if (bccomp($transaction->transaction_amount, '0') === -1) {
713
                    return $transaction;
714
                }
715
716
                return null;
717
            }
718
        );
719
720
        return (string)$set->sum('transaction_amount');
721
    }
722
723
    /**
724
     * @param array $data
725
     *
726
     * @return Budget
727
     */
728
    public function store(array $data): Budget
729
    {
730
        $newBudget = new Budget(
731
            [
732
                'user_id' => $this->user->id,
733
                'name'    => $data['name'],
734
            ]
735
        );
736
        $newBudget->save();
737
738
        return $newBudget;
739
    }
740
741
    /**
742
     * @param array $data
743
     *
744
     * @throws FireflyException
745
     * @return BudgetLimit
746
     */
747
    public function storeBudgetLimit(array $data): BudgetLimit
748
    {
749
        $this->cleanupBudgets();
750
        /** @var Budget $budget */
751
        $budget = $data['budget'];
752
753
        // find limit with same date range.
754
        // if it exists, throw error.
755
        $limits = $budget->budgetlimits()
756
                         ->where('budget_limits.start_date', $data['start_date']->format('Y-m-d 00:00:00'))
757
                         ->where('budget_limits.end_date', $data['end_date']->format('Y-m-d 00:00:00'))
758
                         ->get(['budget_limits.*'])->count();
759
        Log::debug(sprintf('Found %d budget limits.', $limits));
760
        if ($limits > 0) {
761
            throw new FireflyException('A budget limit for this budget, and this date range already exists. You must update the existing one.');
762
        }
763
764
        Log::debug('No existing budget limit, create a new one');
765
        // or create one and return it.
766
        $limit = new BudgetLimit;
767
        $limit->budget()->associate($budget);
768
        $limit->start_date = $data['start_date']->format('Y-m-d 00:00:00');
769
        $limit->end_date   = $data['end_date']->format('Y-m-d 00:00:00');
770
        $limit->amount     = $data['amount'];
771
        $limit->save();
772
        Log::debug(sprintf('Created new budget limit with ID #%d and amount %s', $limit->id, $data['amount']));
773
774
        return $limit;
775
    }
776
777
    /**
778
     * @param Budget $budget
779
     * @param array  $data
780
     *
781
     * @return Budget
782
     */
783
    public function update(Budget $budget, array $data): Budget
784
    {
785
        $oldName        = $budget->name;
786
        $budget->name   = $data['name'];
787
        $budget->active = $data['active'];
788
        $budget->save();
789
        $this->updateRuleTriggers($oldName, $data['name']);
790
        $this->updateRuleActions($oldName, $data['name']);
791
        app('preferences')->mark();
792
793
        return $budget;
794
    }
795
796
    /**
797
     * @param AvailableBudget $availableBudget
798
     * @param array           $data
799
     *
800
     * @return AvailableBudget
801
     * @throws FireflyException
802
     */
803
    public function updateAvailableBudget(AvailableBudget $availableBudget, array $data): AvailableBudget
804
    {
805
        $existing = $this->user->availableBudgets()
806
                               ->where('transaction_currency_id', $data['transaction_currency_id'])
807
                               ->where('start_date', $data['start_date']->format('Y-m-d 00:00:00'))
808
                               ->where('end_date', $data['end_date']->format('Y-m-d 00:00:00'))
809
                               ->where('id', '!=', $availableBudget->id)
810
                               ->first();
811
812
        if (null !== $existing) {
813
            throw new FireflyException(sprintf('An entry already exists for these parameters: available budget object with ID #%d', $existing->id));
814
        }
815
        $availableBudget->transaction_currency_id = $data['transaction_currency_id'];
816
        $availableBudget->start_date              = $data['start_date'];
817
        $availableBudget->end_date                = $data['end_date'];
818
        $availableBudget->amount                  = $data['amount'];
819
        $availableBudget->save();
820
821
        return $availableBudget;
822
823
    }
824
825
    /**
826
     * @param BudgetLimit $budgetLimit
827
     * @param array       $data
828
     *
829
     * @return BudgetLimit
830
     * @throws Exception
831
     */
832
    public function updateBudgetLimit(BudgetLimit $budgetLimit, array $data): BudgetLimit
833
    {
834
        $this->cleanupBudgets();
835
        /** @var Budget $budget */
836
        $budget = $data['budget'];
837
838
        $budgetLimit->budget()->associate($budget);
839
        $budgetLimit->start_date = $data['start_date']->format('Y-m-d 00:00:00');
840
        $budgetLimit->end_date   = $data['end_date']->format('Y-m-d 00:00:00');
841
        $budgetLimit->amount     = $data['amount'];
842
        $budgetLimit->save();
843
        Log::debug(sprintf('Updated budget limit with ID #%d and amount %s', $budgetLimit->id, $data['amount']));
844
845
        return $budgetLimit;
846
    }
847
848
    /** @noinspection MoreThanThreeArgumentsInspection */
849
    /**
850
     * @param Budget $budget
851
     * @param Carbon $start
852
     * @param Carbon $end
853
     * @param string $amount
854
     *
855
     * @return BudgetLimit|null
856
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
857
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
858
     */
859
    public function updateLimitAmount(Budget $budget, Carbon $start, Carbon $end, string $amount): ?BudgetLimit
860
    {
861
        $this->cleanupBudgets();
862
        // count the limits:
863
        $limits = $budget->budgetlimits()
864
                         ->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00'))
865
                         ->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00'))
866
                         ->get(['budget_limits.*'])->count();
867
        Log::debug(sprintf('Found %d budget limits.', $limits));
868
869
        // there might be a budget limit for these dates:
870
        /** @var BudgetLimit $limit */
871
        $limit = $budget->budgetlimits()
872
                        ->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00'))
873
                        ->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00'))
874
                        ->first(['budget_limits.*']);
875
876
        // if more than 1 limit found, delete the others:
877
        if ($limits > 1 && null !== $limit) {
878
            Log::debug(sprintf('Found more than 1, delete all except #%d', $limit->id));
879
            $budget->budgetlimits()
880
                   ->where('budget_limits.start_date', $start->format('Y-m-d 00:00:00'))
881
                   ->where('budget_limits.end_date', $end->format('Y-m-d 00:00:00'))
882
                   ->where('budget_limits.id', '!=', $limit->id)->delete();
883
        }
884
885
        // delete if amount is zero.
886
        // Returns 0 if the two operands are equal,
887
        // 1 if the left_operand is larger than the right_operand, -1 otherwise.
888
        if (null !== $limit && bccomp($amount, '0') <= 0) {
889
            Log::debug(sprintf('%s is zero, delete budget limit #%d', $amount, $limit->id));
890
            try {
891
                $limit->delete();
892
            } catch (Exception $e) {
893
                Log::debug(sprintf('Could not delete limit: %s', $e->getMessage()));
894
            }
895
896
897
            return null;
898
        }
899
        // update if exists:
900
        if (null !== $limit) {
901
            Log::debug(sprintf('Existing budget limit is #%d, update this to amount %s', $limit->id, $amount));
902
            $limit->amount = $amount;
903
            $limit->save();
904
905
            return $limit;
906
        }
907
        Log::debug('No existing budget limit, create a new one');
908
        // or create one and return it.
909
        $limit = new BudgetLimit;
910
        $limit->budget()->associate($budget);
911
        $limit->start_date = $start->startOfDay();
912
        $limit->end_date   = $end->startOfDay();
913
        $limit->amount     = $amount;
914
        $limit->save();
915
        Log::debug(sprintf('Created new budget limit with ID #%d and amount %s', $limit->id, $amount));
916
917
        return $limit;
918
    }
919
920
    /**
921
     * @param string $oldName
922
     * @param string $newName
923
     */
924
    private function updateRuleActions(string $oldName, string $newName): void
925
    {
926
        $types   = ['set_budget',];
927
        $actions = RuleAction::leftJoin('rules', 'rules.id', '=', 'rule_actions.rule_id')
928
                             ->where('rules.user_id', $this->user->id)
929
                             ->whereIn('rule_actions.action_type', $types)
930
                             ->where('rule_actions.action_value', $oldName)
931
                             ->get(['rule_actions.*']);
932
        Log::debug(sprintf('Found %d actions to update.', $actions->count()));
933
        /** @var RuleAction $action */
934
        foreach ($actions as $action) {
935
            $action->action_value = $newName;
936
            $action->save();
937
            Log::debug(sprintf('Updated action %d: %s', $action->id, $action->action_value));
938
        }
939
    }
940
941
    /**
942
     * @param string $oldName
943
     * @param string $newName
944
     */
945
    private function updateRuleTriggers(string $oldName, string $newName): void
946
    {
947
        $types    = ['budget_is',];
948
        $triggers = RuleTrigger::leftJoin('rules', 'rules.id', '=', 'rule_triggers.rule_id')
949
                               ->where('rules.user_id', $this->user->id)
950
                               ->whereIn('rule_triggers.trigger_type', $types)
951
                               ->where('rule_triggers.trigger_value', $oldName)
952
                               ->get(['rule_triggers.*']);
953
        Log::debug(sprintf('Found %d triggers to update.', $triggers->count()));
954
        /** @var RuleTrigger $trigger */
955
        foreach ($triggers as $trigger) {
956
            $trigger->trigger_value = $newName;
957
            $trigger->save();
958
            Log::debug(sprintf('Updated trigger %d: %s', $trigger->id, $trigger->trigger_value));
959
        }
960
    }
961
}
962