RecurringRepository   F
last analyzed

Complexity

Total Complexity 61

Size/Duplication

Total Lines 486
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 61
eloc 175
c 2
b 0
f 0
dl 0
loc 486
rs 3.52

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 2
A getAll() 0 8 1
A getBudget() 0 11 4
A get() 0 8 1
A getCategory() 0 11 4
A destroy() 0 5 1
A getJournalCount() 0 16 3
A getJournalIds() 0 7 1
A getNoteText() 0 9 2
A getTransactionPaginator() 0 22 2
A getOccurrencesInRange() 0 30 6
A update() 0 6 1
A setUser() 0 3 1
A store() 0 11 2
A getPiggyBank() 0 11 3
A getXOccurrences() 0 24 6
A getTransactions() 0 28 3
B repetitionDescription() 0 48 8
A getTags() 0 12 4
A getXOccurrencesSince() 0 25 6

How to fix   Complexity   

Complex Class

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

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

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

1
<?php
2
/**
3
 * RecurringRepository.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\Repositories\Recurring;
25
26
use Carbon\Carbon;
27
use FireflyIII\Exceptions\FireflyException;
28
use FireflyIII\Factory\RecurrenceFactory;
29
use FireflyIII\Helpers\Collector\GroupCollectorInterface;
30
use FireflyIII\Models\Note;
31
use FireflyIII\Models\Preference;
32
use FireflyIII\Models\Recurrence;
33
use FireflyIII\Models\RecurrenceMeta;
34
use FireflyIII\Models\RecurrenceRepetition;
35
use FireflyIII\Models\RecurrenceTransaction;
36
use FireflyIII\Models\RecurrenceTransactionMeta;
37
use FireflyIII\Models\TransactionJournal;
38
use FireflyIII\Models\TransactionJournalMeta;
39
use FireflyIII\Services\Internal\Destroy\RecurrenceDestroyService;
40
use FireflyIII\Services\Internal\Update\RecurrenceUpdateService;
41
use FireflyIII\Support\Repositories\Recurring\CalculateRangeOccurrences;
42
use FireflyIII\Support\Repositories\Recurring\CalculateXOccurrences;
43
use FireflyIII\Support\Repositories\Recurring\CalculateXOccurrencesSince;
44
use FireflyIII\Support\Repositories\Recurring\FiltersWeekends;
45
use FireflyIII\User;
46
use Illuminate\Pagination\LengthAwarePaginator;
47
use Illuminate\Support\Collection;
48
use Log;
49
50
/**
51
 * Class RecurringRepository
52
 */
53
class RecurringRepository implements RecurringRepositoryInterface
54
{
55
    use CalculateRangeOccurrences, CalculateXOccurrences, CalculateXOccurrencesSince, FiltersWeekends;
0 ignored issues
show
introduced by James Cole
The trait FireflyIII\Support\Repos...g\CalculateXOccurrences requires some properties which are not provided by FireflyIII\Repositories\...ing\RecurringRepository: $daysInMonth, $dayOfWeekIso
Loading history...
introduced by James Cole
The trait FireflyIII\Support\Repos...culateXOccurrencesSince requires some properties which are not provided by FireflyIII\Repositories\...ing\RecurringRepository: $daysInMonth, $dayOfWeekIso
Loading history...
introduced by James Cole
The trait FireflyIII\Support\Repos...lculateRangeOccurrences requires some properties which are not provided by FireflyIII\Repositories\...ing\RecurringRepository: $daysInMonth, $dayOfWeekIso
Loading history...
introduced by James Cole
The trait FireflyIII\Support\Repos...curring\FiltersWeekends requires some properties which are not provided by FireflyIII\Repositories\...ing\RecurringRepository: $weekend, $dayOfWeekIso
Loading history...
56
    /** @var User */
57
    private $user;
58
59
    /**
60
     * Constructor.
61
     */
62
    public function __construct()
63
    {
64
        if ('testing' === config('app.env')) {
65
            Log::warning(sprintf('%s should not be instantiated in the TEST environment!', get_class($this)));
66
        }
67
    }
68
69
    /**
70
     * Destroy a recurring transaction.
71
     *
72
     * @param Recurrence $recurrence
73
     */
74
    public function destroy(Recurrence $recurrence): void
75
    {
76
        /** @var RecurrenceDestroyService $service */
77
        $service = app(RecurrenceDestroyService::class);
78
        $service->destroy($recurrence);
79
    }
80
81
    /**
82
     * Returns all of the user's recurring transactions.
83
     *
84
     * @return Collection
85
     */
86
    public function get(): Collection
87
    {
88
        return $this->user->recurrences()
89
                          ->with(['TransactionCurrency', 'TransactionType', 'RecurrenceRepetitions', 'RecurrenceTransactions'])
90
                          ->orderBy('active', 'DESC')
91
                          ->orderBy('transaction_type_id', 'ASC')
92
                          ->orderBy('title', 'ASC')
93
                          ->get();
94
    }
95
96
    /**
97
     * Get ALL recurring transactions.
98
     *
99
     * @return Collection
100
     */
101
    public function getAll(): Collection
102
    {
103
        // grab ALL recurring transactions:
104
        return Recurrence
105
            ::with(['TransactionCurrency', 'TransactionType', 'RecurrenceRepetitions', 'RecurrenceTransactions'])
106
            ->orderBy('active', 'DESC')
107
            ->orderBy('title', 'ASC')
108
            ->get();
109
    }
110
111
    /**
112
     * Get the budget ID from a recurring transaction transaction.
113
     *
114
     * @param RecurrenceTransaction $recTransaction
115
     *
116
     * @return null|int
117
     */
118
    public function getBudget(RecurrenceTransaction $recTransaction): ?int
119
    {
120
        $return = 0;
121
        /** @var RecurrenceTransactionMeta $meta */
122
        foreach ($recTransaction->recurrenceTransactionMeta as $meta) {
123
            if ('budget_id' === $meta->name) {
124
                $return = (int)$meta->value;
125
            }
126
        }
127
128
        return 0 === $return ? null : $return;
129
    }
130
131
    /**
132
     * Get the category from a recurring transaction transaction.
133
     *
134
     * @param RecurrenceTransaction $recTransaction
135
     *
136
     * @return null|string
137
     */
138
    public function getCategory(RecurrenceTransaction $recTransaction): ?string
139
    {
140
        $return = '';
141
        /** @var RecurrenceTransactionMeta $meta */
142
        foreach ($recTransaction->recurrenceTransactionMeta as $meta) {
143
            if ('category_name' === $meta->name) {
144
                $return = (string)$meta->value;
145
            }
146
        }
147
148
        return '' === $return ? null : $return;
149
    }
150
151
    /**
152
     * Returns the journals created for this recurrence, possibly limited by time.
153
     *
154
     * @param Recurrence  $recurrence
155
     * @param Carbon|null $start
156
     * @param Carbon|null $end
157
     *
158
     * @return int
159
     */
160
    public function getJournalCount(Recurrence $recurrence, Carbon $start = null, Carbon $end = null): int
161
    {
162
        $query = TransactionJournal
163
            ::leftJoin('journal_meta', 'journal_meta.transaction_journal_id', '=', 'transaction_journals.id')
164
            ->where('transaction_journals.user_id', $recurrence->user_id)
165
            ->whereNull('transaction_journals.deleted_at')
166
            ->where('journal_meta.name', 'recurrence_id')
167
            ->where('journal_meta.data', '"' . $recurrence->id . '"');
168
        if (null !== $start) {
169
            $query->where('transaction_journals.date', '>=', $start->format('Y-m-d 00:00:00'));
170
        }
171
        if (null !== $end) {
172
            $query->where('transaction_journals.date', '<=', $end->format('Y-m-d 00:00:00'));
173
        }
174
175
        return $query->get(['transaction_journals.*'])->count();
176
    }
177
178
    /**
179
     * Get journal ID's for journals created by this recurring transaction.
180
     *
181
     * @param Recurrence $recurrence
182
     *
183
     * @return array
184
     */
185
    public function getJournalIds(Recurrence $recurrence): array
186
    {
187
        return TransactionJournalMeta::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id')
188
                                     ->where('transaction_journals.user_id', $this->user->id)
189
                                     ->where('journal_meta.name', '=', 'recurrence_id')
190
                                     ->where('journal_meta.data', '=', json_encode((string)$recurrence->id))
191
                                     ->get(['journal_meta.transaction_journal_id'])->pluck('transaction_journal_id')->toArray();
192
    }
193
194
    /**
195
     * Get the notes.
196
     *
197
     * @param Recurrence $recurrence
198
     *
199
     * @return string
200
     */
201
    public function getNoteText(Recurrence $recurrence): string
202
    {
203
        /** @var Note $note */
204
        $note = $recurrence->notes()->first();
205
        if (null !== $note) {
206
            return (string)$note->text;
207
        }
208
209
        return '';
210
    }
211
212
    /**
213
     * Generate events in the date range.
214
     *
215
     * @param RecurrenceRepetition $repetition
216
     * @param Carbon               $start
217
     * @param Carbon               $end
218
     *
219
     * @return array
220
     *
221
     */
222
    public function getOccurrencesInRange(RecurrenceRepetition $repetition, Carbon $start, Carbon $end): array
223
    {
224
        $occurrences = [];
225
        $mutator     = clone $start;
226
        $mutator->startOfDay();
227
        $skipMod = $repetition->repetition_skip + 1;
228
        Log::debug(sprintf('Calculating occurrences for rep type "%s"', $repetition->repetition_type));
229
        Log::debug(sprintf('Mutator is now: %s', $mutator->format('Y-m-d')));
230
231
        if ('daily' === $repetition->repetition_type) {
232
            $occurrences = $this->getDailyInRange($mutator, $end, $skipMod);
233
        }
234
        if ('weekly' === $repetition->repetition_type) {
235
            $occurrences = $this->getWeeklyInRange($mutator, $end, $skipMod, $repetition->repetition_moment);
236
        }
237
        if ('monthly' === $repetition->repetition_type) {
238
            $occurrences = $this->getMonthlyInRange($mutator, $end, $skipMod, $repetition->repetition_moment);
239
        }
240
        if ('ndom' === $repetition->repetition_type) {
241
            $occurrences = $this->getNdomInRange($mutator, $end, $skipMod, $repetition->repetition_moment);
242
        }
243
        if ('yearly' === $repetition->repetition_type) {
244
            $occurrences = $this->getYearlyInRange($mutator, $end, $skipMod, $repetition->repetition_moment);
245
        }
246
247
248
        // filter out all the weekend days:
249
        $occurrences = $this->filterWeekends($repetition, $occurrences);
250
251
        return $occurrences;
252
    }
253
254
    /**
255
     * @param RecurrenceTransaction $transaction
256
     *
257
     * @return int|null
258
     */
259
    public function getPiggyBank(RecurrenceTransaction $transaction): ?int
260
    {
261
        $meta = $transaction->recurrenceTransactionMeta;
262
        /** @var RecurrenceTransactionMeta $metaEntry */
263
        foreach ($meta as $metaEntry) {
264
            if ('piggy_bank_id' === $metaEntry->name) {
265
                return (int)$metaEntry->value;
266
            }
267
        }
268
269
        return null;
270
    }
271
272
    /**
273
     * Get the tags from the recurring transaction.
274
     *
275
     * @param RecurrenceTransaction $transaction
276
     *
277
     * @return array
278
     */
279
    public function getTags(RecurrenceTransaction $transaction): array
280
    {
281
        $tags = [];
282
        /** @var RecurrenceMeta $meta */
283
        foreach ($transaction->recurrenceTransactionMeta as $meta) {
284
            if ('tags' === $meta->name && '' !== $meta->value) {
285
                //$tags = explode(',', $meta->value);
286
                $tags = json_decode($meta->value, true, 512, JSON_THROW_ON_ERROR);
287
            }
288
        }
289
290
        return $tags;
291
    }
292
293
    /**
294
     * @param Recurrence $recurrence
295
     * @param int        $page
296
     * @param int        $pageSize
297
     *
298
     * @return LengthAwarePaginator
299
     */
300
    public function getTransactionPaginator(Recurrence $recurrence, int $page, int $pageSize): LengthAwarePaginator
301
    {
302
        $journalMeta = TransactionJournalMeta
303
            ::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id')
304
            ->whereNull('transaction_journals.deleted_at')
305
            ->where('transaction_journals.user_id', $this->user->id)
306
            ->where('name', 'recurrence_id')
307
            ->where('data', json_encode((string)$recurrence->id))
308
            ->get()->pluck('transaction_journal_id')->toArray();
309
        $search      = [];
310
        foreach ($journalMeta as $journalId) {
311
            $search[] = (int)$journalId;
312
        }
313
        /** @var GroupCollectorInterface $collector */
314
        $collector = app(GroupCollectorInterface::class);
315
316
        $collector->setUser($recurrence->user);
317
        $collector->withCategoryInformation()->withBudgetInformation()->setLimit($pageSize)->setPage($page)
318
                  ->withAccountInformation();
319
        $collector->setJournalIds($search);
320
321
        return $collector->getPaginatedGroups();
322
    }
323
324
    /**
325
     * @param Recurrence $recurrence
326
     *
327
     * @return Collection
328
     */
329
    public function getTransactions(Recurrence $recurrence): Collection
330
    {
331
        $journalMeta = TransactionJournalMeta
332
            ::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id')
333
            ->whereNull('transaction_journals.deleted_at')
334
            ->where('transaction_journals.user_id', $this->user->id)
335
            ->where('name', 'recurrence_id')
336
            ->where('data', json_encode((string)$recurrence->id))
337
            ->get()->pluck('transaction_journal_id')->toArray();
338
        $search      = [];
339
340
        foreach ($journalMeta as $journalId) {
341
            $search[] = (int)$journalId;
342
        }
343
        if (0 === count($search)) {
344
345
            return new Collection;
346
        }
347
348
        /** @var GroupCollectorInterface $collector */
349
        $collector = app(GroupCollectorInterface::class);
350
351
        $collector->setUser($recurrence->user);
352
        $collector->withCategoryInformation()->withBudgetInformation()->withAccountInformation();
353
        // filter on specific journals.
354
        $collector->setJournalIds($search);
355
356
        return $collector->getGroups();
357
    }
358
359
    /**
360
     * Calculate the next X iterations starting on the date given in $date.
361
     *
362
     * @param RecurrenceRepetition $repetition
363
     * @param Carbon               $date
364
     * @param int                  $count
365
     *
366
     * @return array
367
     *
368
     */
369
    public function getXOccurrences(RecurrenceRepetition $repetition, Carbon $date, int $count): array
370
    {
371
        $skipMod     = $repetition->repetition_skip + 1;
372
        $occurrences = [];
373
        if ('daily' === $repetition->repetition_type) {
374
            $occurrences = $this->getXDailyOccurrences($date, $count, $skipMod);
375
        }
376
        if ('weekly' === $repetition->repetition_type) {
377
            $occurrences = $this->getXWeeklyOccurrences($date, $count, $skipMod, $repetition->repetition_moment);
378
        }
379
        if ('monthly' === $repetition->repetition_type) {
380
            $occurrences = $this->getXMonthlyOccurrences($date, $count, $skipMod, $repetition->repetition_moment);
381
        }
382
        if ('ndom' === $repetition->repetition_type) {
383
            $occurrences = $this->getXNDomOccurrences($date, $count, $skipMod, $repetition->repetition_moment);
384
        }
385
        if ('yearly' === $repetition->repetition_type) {
386
            $occurrences = $this->getXYearlyOccurrences($date, $count, $skipMod, $repetition->repetition_moment);
387
        }
388
389
        // filter out all the weekend days:
390
        $occurrences = $this->filterWeekends($repetition, $occurrences);
391
392
        return $occurrences;
393
    }
394
395
    /**
396
     * Parse the repetition in a string that is user readable.
397
     *
398
     * @param RecurrenceRepetition $repetition
399
     *
400
     * @return string
401
     *
402
     */
403
    public function repetitionDescription(RecurrenceRepetition $repetition): string
404
    {
405
        Log::debug('Now in repetitionDescription()');
406
        /** @var Preference $pref */
407
        $pref     = app('preferences')->getForUser($this->user, 'language', config('firefly.default_language', 'en_US'));
408
        $language = $pref->data;
409
        if ('daily' === $repetition->repetition_type) {
410
            return (string)trans('firefly.recurring_daily', [], $language);
411
        }
412
        if ('weekly' === $repetition->repetition_type) {
413
414
            $dayOfWeek = trans(sprintf('config.dow_%s', $repetition->repetition_moment), [], $language);
415
            if ($repetition->repetition_skip > 0) {
416
                return (string)trans('firefly.recurring_weekly_skip', ['weekday' => $dayOfWeek, 'skip' => $repetition->repetition_skip + 1], $language);
417
            }
418
419
            return (string)trans('firefly.recurring_weekly', ['weekday' => $dayOfWeek], $language);
420
        }
421
        if ('monthly' === $repetition->repetition_type) {
422
            if ($repetition->repetition_skip > 0) {
423
                return (string)trans(
424
                    'firefly.recurring_monthly_skip', ['dayOfMonth' => $repetition->repetition_moment, 'skip' => $repetition->repetition_skip + 1], $language
425
                );
426
            }
427
428
            return (string)trans(
429
                'firefly.recurring_monthly', ['dayOfMonth' => $repetition->repetition_moment, 'skip' => $repetition->repetition_skip - 1], $language
430
            );
431
        }
432
        if ('ndom' === $repetition->repetition_type) {
433
            $parts = explode(',', $repetition->repetition_moment);
434
            // first part is number of week, second is weekday.
435
            $dayOfWeek = trans(sprintf('config.dow_%s', $parts[1]), [], $language);
436
437
            return (string)trans('firefly.recurring_ndom', ['weekday' => $dayOfWeek, 'dayOfMonth' => $parts[0]], $language);
438
        }
439
        if ('yearly' === $repetition->repetition_type) {
440
            //
441
            $today       = Carbon::now()->endOfYear();
442
            $repDate     = Carbon::createFromFormat('Y-m-d', $repetition->repetition_moment);
443
            $diffInYears = $today->diffInYears($repDate);
444
            $repDate->addYears($diffInYears); // technically not necessary.
445
            $string = $repDate->formatLocalized((string)trans('config.month_and_day_no_year'));
446
447
            return (string)trans('firefly.recurring_yearly', ['date' => $string], $language);
448
        }
449
450
        return '';
451
452
    }
453
454
    /**
455
     * Set user for in repository.
456
     *
457
     * @param User $user
458
     */
459
    public function setUser(User $user): void
460
    {
461
        $this->user = $user;
462
    }
463
464
    /**
465
     * @param array $data
466
     *
467
     * @return Recurrence
468
     * @throws FireflyException
469
     */
470
    public function store(array $data): Recurrence
471
    {
472
        /** @var RecurrenceFactory $factory */
473
        $factory = app(RecurrenceFactory::class);
474
        $factory->setUser($this->user);
475
        $result = $factory->create($data);
476
        if (null === $result) {
477
            throw new FireflyException($factory->getErrors()->first());
0 ignored issues
show
Bug introduced by James Cole
The method getErrors() does not exist on FireflyIII\Factory\RecurrenceFactory. ( Ignorable by Annotation )

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

477
            throw new FireflyException($factory->/** @scrutinizer ignore-call */ getErrors()->first());

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...
478
        }
479
480
        return $result;
481
    }
482
483
    /**
484
     * Update a recurring transaction.
485
     *
486
     * @param Recurrence $recurrence
487
     * @param array      $data
488
     *
489
     * @return Recurrence
490
     * @throws FireflyException
491
     */
492
    public function update(Recurrence $recurrence, array $data): Recurrence
493
    {
494
        /** @var RecurrenceUpdateService $service */
495
        $service = app(RecurrenceUpdateService::class);
496
497
        return $service->update($recurrence, $data);
498
    }
499
500
    /**
501
     * Calculate the next X iterations starting on the date given in $date.
502
     * Returns an array of Carbon objects.
503
     *
504
     * Only returns them of they are after $afterDate
505
     *
506
     * @param RecurrenceRepetition $repetition
507
     * @param Carbon               $date
508
     * @param Carbon               $afterDate
509
     * @param int                  $count
510
     *
511
     * @return array
512
     * @throws FireflyException
513
     */
514
    public function getXOccurrencesSince(RecurrenceRepetition $repetition, Carbon $date, Carbon $afterDate, int $count): array
515
    {
516
        Log::debug('Now in getXOccurrencesSince()');
517
        $skipMod     = $repetition->repetition_skip + 1;
518
        $occurrences = [];
519
        if ('daily' === $repetition->repetition_type) {
520
            $occurrences = $this->getXDailyOccurrencesSince($date, $afterDate, $count, $skipMod);
521
        }
522
        if ('weekly' === $repetition->repetition_type) {
523
            $occurrences = $this->getXWeeklyOccurrencesSince($date, $afterDate, $count, $skipMod, $repetition->repetition_moment);
524
        }
525
        if ('monthly' === $repetition->repetition_type) {
526
            $occurrences = $this->getXMonthlyOccurrencesSince($date, $afterDate, $count, $skipMod, $repetition->repetition_moment);
527
        }
528
        if ('ndom' === $repetition->repetition_type) {
529
            $occurrences = $this->getXNDomOccurrencesSince($date, $afterDate, $count, $skipMod, $repetition->repetition_moment);
530
        }
531
        if ('yearly' === $repetition->repetition_type) {
532
            $occurrences = $this->getXYearlyOccurrencesSince($date, $afterDate, $count, $skipMod, $repetition->repetition_moment);
533
        }
534
535
        // filter out all the weekend days:
536
        $occurrences = $this->filterWeekends($repetition, $occurrences);
537
538
        return $occurrences;
539
    }
540
}
541