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

RecurringRepository::__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
 * RecurringRepository.php
4
 * Copyright (c) 2018 [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
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\TransactionCollectorInterface;
30
use FireflyIII\Helpers\Filter\InternalTransferFilter;
31
use FireflyIII\Models\Note;
32
use FireflyIII\Models\Preference;
33
use FireflyIII\Models\Recurrence;
34
use FireflyIII\Models\RecurrenceMeta;
35
use FireflyIII\Models\RecurrenceRepetition;
36
use FireflyIII\Models\RecurrenceTransaction;
37
use FireflyIII\Models\RecurrenceTransactionMeta;
38
use FireflyIII\Models\TransactionJournal;
39
use FireflyIII\Models\TransactionJournalMeta;
40
use FireflyIII\Services\Internal\Destroy\RecurrenceDestroyService;
41
use FireflyIII\Services\Internal\Update\RecurrenceUpdateService;
42
use FireflyIII\User;
43
use Illuminate\Pagination\LengthAwarePaginator;
44
use Illuminate\Support\Collection;
45
use Log;
46
47
/**
48
 *
49
 * Class RecurringRepository
50
 *
51
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
52
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
53
 */
54
class RecurringRepository implements RecurringRepositoryInterface
55
{
56
    /** @var User */
57
    private $user;
58
59
    /**
60
     * Constructor.
61
     */
62
    public function __construct()
63
    {
64
        if ('testing' === env('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 the notes.
180
     *
181
     * @param Recurrence $recurrence
182
     *
183
     * @return string
184
     */
185
    public function getNoteText(Recurrence $recurrence): string
186
    {
187
        /** @var Note $note */
188
        $note = $recurrence->notes()->first();
189
        if (null !== $note) {
190
            return (string)$note->text;
191
        }
192
193
        return '';
194
    }
195
196
    /**
197
     * Generate events in the date range.
198
     *
199
     * @param RecurrenceRepetition $repetition
200
     * @param Carbon               $start
201
     * @param Carbon               $end
202
     *
203
     *
204
     * @return array
205
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
206
     */
207
    public function getOccurrencesInRange(RecurrenceRepetition $repetition, Carbon $start, Carbon $end): array
208
    {
209
        $occurrences = [];
210
        $mutator     = clone $start;
211
        $mutator->startOfDay();
212
        $skipMod = $repetition->repetition_skip + 1;
213
        Log::debug(sprintf('Calculating occurrences for rep type "%s"', $repetition->repetition_type));
214
        Log::debug(sprintf('Mutator is now: %s', $mutator->format('Y-m-d')));
215
216
        if ('daily' === $repetition->repetition_type) {
217
            $occurrences = $this->getDailyInRange($mutator, $end, $skipMod);
218
        }
219
        if ('weekly' === $repetition->repetition_type) {
220
            $occurrences = $this->getWeeklyInRange($mutator, $end, $skipMod, $repetition->repetition_moment);
221
        }
222
        if ('monthly' === $repetition->repetition_type) {
223
            $occurrences = $this->getMonthlyInRange($mutator, $end, $skipMod, $repetition->repetition_moment);
224
        }
225
        if ('ndom' === $repetition->repetition_type) {
226
            $occurrences = $this->getNdomInRange($mutator, $end, $skipMod, $repetition->repetition_moment);
227
        }
228
        if ('yearly' === $repetition->repetition_type) {
229
            $occurrences = $this->getYearlyInRange($mutator, $end, $skipMod, $repetition->repetition_moment);
230
        }
231
232
233
        // filter out all the weekend days:
234
        $occurrences = $this->filterWeekends($repetition, $occurrences);
235
236
        return $occurrences;
237
    }
238
239
    /**
240
     * Get the tags from the recurring transaction.
241
     *
242
     * @param Recurrence $recurrence
243
     *
244
     * @return array
245
     */
246
    public function getTags(Recurrence $recurrence): array
247
    {
248
        $tags = [];
249
        /** @var RecurrenceMeta $meta */
250
        foreach ($recurrence->recurrenceMeta as $meta) {
251
            if ('tags' === $meta->name && '' !== $meta->value) {
252
                $tags = explode(',', $meta->value);
253
            }
254
        }
255
256
        return $tags;
257
    }
258
259
    /**
260
     * @param Recurrence $recurrence
261
     * @param int        $page
262
     * @param int        $pageSize
263
     *
264
     * @return LengthAwarePaginator
265
     */
266
    public function getTransactionPaginator(Recurrence $recurrence, int $page, int $pageSize): LengthAwarePaginator
267
    {
268
        $journalMeta = TransactionJournalMeta
269
            ::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id')
270
            ->whereNull('transaction_journals.deleted_at')
271
            ->where('transaction_journals.user_id', $this->user->id)
272
            ->where('name', 'recurrence_id')
273
            ->where('data', json_encode((string)$recurrence->id))
274
            ->get()->pluck('transaction_journal_id')->toArray();
275
        $search      = [];
276
        foreach ($journalMeta as $journalId) {
277
            $search[] = ['id' => (int)$journalId];
278
        }
279
        /** @var TransactionCollectorInterface $collector */
280
        $collector = app(TransactionCollectorInterface::class);
281
        $collector->setUser($recurrence->user);
282
        $collector->withOpposingAccount()->setAllAssetAccounts()->withCategoryInformation()->withBudgetInformation()->setLimit($pageSize)->setPage($page);
283
        // filter on specific journals.
284
        $collector->removeFilter(InternalTransferFilter::class);
285
        $collector->setJournals(new Collection($search));
286
287
        return $collector->getPaginatedTransactions();
288
    }
289
290
    /**
291
     * @param Recurrence $recurrence
292
     *
293
     * @return Collection
294
     */
295
    public function getTransactions(Recurrence $recurrence): Collection
296
    {
297
        $journalMeta = TransactionJournalMeta
298
            ::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id')
299
            ->whereNull('transaction_journals.deleted_at')
300
            ->where('transaction_journals.user_id', $this->user->id)
301
            ->where('name', 'recurrence_id')
302
            ->where('data', json_encode((string)$recurrence->id))
303
            ->get()->pluck('transaction_journal_id')->toArray();
304
        $search      = [];
305
        foreach ($journalMeta as $journalId) {
306
            $search[] = ['id' => (int)$journalId];
307
        }
308
        /** @var TransactionCollectorInterface $collector */
309
        $collector = app(TransactionCollectorInterface::class);
310
        $collector->setUser($recurrence->user);
311
        $collector->withOpposingAccount()->setAllAssetAccounts()->withCategoryInformation()->withBudgetInformation();
312
        // filter on specific journals.
313
        $collector->removeFilter(InternalTransferFilter::class);
314
        $collector->setJournals(new Collection($search));
315
316
        return $collector->getTransactions();
317
    }
318
319
    /**
320
     * Calculate the next X iterations starting on the date given in $date.
321
     *
322
     * @param RecurrenceRepetition $repetition
323
     * @param Carbon               $date
324
     * @param int                  $count
325
     *
326
     * @return array
327
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
328
     */
329
    public function getXOccurrences(RecurrenceRepetition $repetition, Carbon $date, int $count): array
330
    {
331
        $skipMod     = $repetition->repetition_skip + 1;
332
        $occurrences = [];
333
        if ('daily' === $repetition->repetition_type) {
334
            $occurrences = $this->getXDailyOccurrences($date, $count, $skipMod);
335
        }
336
        if ('weekly' === $repetition->repetition_type) {
337
            $occurrences = $this->getXWeeklyOccurrences($date, $count, $skipMod, $repetition->repetition_moment);
338
        }
339
        if ('monthly' === $repetition->repetition_type) {
340
            $occurrences = $this->getXMonthlyOccurrences($date, $count, $skipMod, $repetition->repetition_moment);
341
        }
342
        if ('ndom' === $repetition->repetition_type) {
343
            $occurrences = $this->getXNDomOccurrences($date, $count, $skipMod, $repetition->repetition_moment);
344
        }
345
        if ('yearly' === $repetition->repetition_type) {
346
            $occurrences = $this->getXYearlyOccurrences($date, $count, $skipMod, $repetition->repetition_moment);
347
        }
348
349
        // filter out all the weekend days:
350
        $occurrences = $this->filterWeekends($repetition, $occurrences);
351
352
        return $occurrences;
353
    }
354
355
    /**
356
     * Parse the repetition in a string that is user readable.
357
     *
358
     * @param RecurrenceRepetition $repetition
359
     *
360
     * @return string
361
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
362
     */
363
    public function repetitionDescription(RecurrenceRepetition $repetition): string
364
    {
365
        /** @var Preference $pref */
366
        $pref     = app('preferences')->getForUser($this->user, 'language', config('firefly.default_language', 'en_US'));
367
        $language = $pref->data;
368
        if ('daily' === $repetition->repetition_type) {
369
            return (string)trans('firefly.recurring_daily', [], $language);
370
        }
371
        if ('weekly' === $repetition->repetition_type) {
372
            $dayOfWeek = trans(sprintf('config.dow_%s', $repetition->repetition_moment), [], $language);
373
374
            return (string)trans('firefly.recurring_weekly', ['weekday' => $dayOfWeek], $language);
375
        }
376
        if ('monthly' === $repetition->repetition_type) {
377
            return (string)trans('firefly.recurring_monthly', ['dayOfMonth' => $repetition->repetition_moment], $language);
378
        }
379
        if ('ndom' === $repetition->repetition_type) {
380
            $parts = explode(',', $repetition->repetition_moment);
381
            // first part is number of week, second is weekday.
382
            $dayOfWeek = trans(sprintf('config.dow_%s', $parts[1]), [], $language);
383
384
            return (string)trans('firefly.recurring_ndom', ['weekday' => $dayOfWeek, 'dayOfMonth' => $parts[0]], $language);
385
        }
386
        if ('yearly' === $repetition->repetition_type) {
387
            //
388
            $today       = Carbon::now()->endOfYear();
389
            $repDate     = Carbon::createFromFormat('Y-m-d', $repetition->repetition_moment);
390
            $diffInYears = $today->diffInYears($repDate);
391
            $repDate->addYears($diffInYears); // technically not necessary.
392
            $string = $repDate->formatLocalized((string)trans('config.month_and_day_no_year'));
393
394
            return (string)trans('firefly.recurring_yearly', ['date' => $string], $language);
395
        }
396
397
        return '';
398
399
    }
400
401
    /**
402
     * Set user for in repository.
403
     *
404
     * @param User $user
405
     */
406
    public function setUser(User $user): void
407
    {
408
        $this->user = $user;
409
    }
410
411
    /**
412
     * @param array $data
413
     *
414
     * @return Recurrence
415
     */
416
    public function store(array $data): Recurrence
417
    {
418
        $factory = new RecurrenceFactory;
419
        $factory->setUser($this->user);
420
421
        return $factory->create($data);
422
    }
423
424
    /**
425
     * Update a recurring transaction.
426
     *
427
     * @param Recurrence $recurrence
428
     * @param array      $data
429
     *
430
     * @return Recurrence
431
     * @throws FireflyException
432
     */
433
    public function update(Recurrence $recurrence, array $data): Recurrence
434
    {
435
        /** @var RecurrenceUpdateService $service */
436
        $service = app(RecurrenceUpdateService::class);
437
438
        return $service->update($recurrence, $data);
439
    }
440
441
    /**
442
     * Filters out all weekend entries, if necessary.
443
     *
444
     * @param RecurrenceRepetition $repetition
445
     * @param array                $dates
446
     *
447
     * @return array
448
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
449
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
450
     */
451
    private function filterWeekends(RecurrenceRepetition $repetition, array $dates): array
452
    {
453
        if ((int)$repetition->weekend === RecurrenceRepetition::WEEKEND_DO_NOTHING) {
454
            Log::debug('Repetition will not be filtered on weekend days.');
455
456
            return $dates;
457
        }
458
        $return = [];
459
        /** @var Carbon $date */
460
        foreach ($dates as $date) {
461
            $isWeekend = $date->isWeekend();
462
            if (!$isWeekend) {
463
                $return[] = clone $date;
464
                Log::debug(sprintf('Date is %s, not a weekend date.', $date->format('D d M Y')));
465
                continue;
466
            }
467
468
            // is weekend and must set back to Friday?
469
            if ($repetition->weekend === RecurrenceRepetition::WEEKEND_TO_FRIDAY) {
470
                $clone = clone $date;
471
                $clone->addDays(5 - $date->dayOfWeekIso);
472
                Log::debug(
473
                    sprintf('Date is %s, and this is in the weekend, so corrected to %s (Friday).', $date->format('D d M Y'), $clone->format('D d M Y'))
474
                );
475
                $return[] = clone $clone;
476
                continue;
477
            }
478
479
            // postpone to Monday?
480
            if ($repetition->weekend === RecurrenceRepetition::WEEKEND_TO_MONDAY) {
481
                $clone = clone $date;
482
                $clone->addDays(8 - $date->dayOfWeekIso);
483
                Log::debug(
484
                    sprintf('Date is %s, and this is in the weekend, so corrected to %s (Monday).', $date->format('D d M Y'), $clone->format('D d M Y'))
485
                );
486
                $return[] = $clone;
487
                continue;
488
            }
489
            Log::debug(sprintf('Date is %s, removed from final result', $date->format('D d M Y')));
490
        }
491
492
        // filter unique dates
493
        Log::debug(sprintf('Count before filtering: %d', \count($dates)));
494
        $collection = new Collection($return);
495
        $filtered   = $collection->unique();
496
        $return     = $filtered->toArray();
497
498
        Log::debug(sprintf('Count after filtering: %d', \count($return)));
499
500
        return $return;
501
    }
502
503
    /**
504
     * Get the number of daily occurrences for a recurring transaction until date $end is reached. Will skip every $skipMod-1 occurrences.
505
     *
506
     * @param Carbon $start
507
     * @param Carbon $end
508
     * @param int    $skipMod
509
     *
510
     * @return array
511
     */
512
    private function getDailyInRange(Carbon $start, Carbon $end, int $skipMod): array
513
    {
514
        $return   = [];
515
        $attempts = 0;
516
        Log::debug('Rep is daily. Start of loop.');
517
        while ($start <= $end) {
518
            Log::debug(sprintf('Mutator is now: %s', $start->format('Y-m-d')));
519
            if (0 === $attempts % $skipMod) {
520
                Log::debug(sprintf('Attempts modulo skipmod is zero, include %s', $start->format('Y-m-d')));
521
                $return[] = clone $start;
522
            }
523
            $start->addDay();
524
            $attempts++;
525
        }
526
527
        return $return;
528
    }
529
530
    /** @noinspection MoreThanThreeArgumentsInspection */
531
    /**
532
     * Get the number of daily occurrences for a recurring transaction until date $end is reached. Will skip every $skipMod-1 occurrences.
533
     *
534
     * @param Carbon $start
535
     * @param Carbon $end
536
     * @param int    $skipMod
537
     * @param string $moment
538
     *
539
     * @return array
540
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
541
     */
542
    private function getMonthlyInRange(Carbon $start, Carbon $end, int $skipMod, string $moment): array
543
    {
544
        $return     = [];
545
        $attempts   = 0;
546
        $dayOfMonth = (int)$moment;
547
        Log::debug(sprintf('Day of month in repetition is %d', $dayOfMonth));
548
        Log::debug(sprintf('Start is %s.', $start->format('Y-m-d')));
549
        Log::debug(sprintf('End is %s.', $end->format('Y-m-d')));
550
        if ($start->day > $dayOfMonth) {
551
            Log::debug('Add a month.');
552
            // day has passed already, add a month.
553
            $start->addMonth();
554
        }
555
        Log::debug(sprintf('Start is now %s.', $start->format('Y-m-d')));
556
        Log::debug('Start loop.');
557
        while ($start < $end) {
558
            Log::debug(sprintf('Mutator is now %s.', $start->format('Y-m-d')));
559
            $domCorrected = min($dayOfMonth, $start->daysInMonth);
560
            Log::debug(sprintf('DoM corrected is %d', $domCorrected));
561
            $start->day = $domCorrected;
562
            Log::debug(sprintf('Mutator is now %s.', $start->format('Y-m-d')));
563
            Log::debug(sprintf('$attempts %% $skipMod === 0 is %s', var_export(0 === $attempts % $skipMod, true)));
564
            Log::debug(sprintf('$start->lte($mutator) is %s', var_export($start->lte($start), true)));
565
            Log::debug(sprintf('$end->gte($mutator) is %s', var_export($end->gte($start), true)));
566
            if (0 === $attempts % $skipMod && $start->lte($start) && $end->gte($start)) {
567
                Log::debug(sprintf('ADD %s to return!', $start->format('Y-m-d')));
568
                $return[] = clone $start;
569
            }
570
            $attempts++;
571
            $start->endOfMonth()->startOfDay()->addDay();
572
        }
573
574
        return $return;
575
    }
576
577
    /** @noinspection MoreThanThreeArgumentsInspection */
578
    /**
579
     * Get the number of daily occurrences for a recurring transaction until date $end is reached. Will skip every $skipMod-1 occurrences.
580
     *
581
     * @param Carbon $start
582
     * @param Carbon $end
583
     * @param int    $skipMod
584
     * @param string $moment
585
     *
586
     * @return array
587
     */
588
    private function getNdomInRange(Carbon $start, Carbon $end, int $skipMod, string $moment): array
589
    {
590
        $return   = [];
591
        $attempts = 0;
592
        $start->startOfMonth();
593
        // this feels a bit like a cop out but why reinvent the wheel?
594
        $counters   = [1 => 'first', 2 => 'second', 3 => 'third', 4 => 'fourth', 5 => 'fifth',];
595
        $daysOfWeek = [1 => 'Monday', 2 => 'Tuesday', 3 => 'Wednesday', 4 => 'Thursday', 5 => 'Friday', 6 => 'Saturday', 7 => 'Sunday',];
596
        $parts      = explode(',', $moment);
597
        while ($start <= $end) {
598
            $string    = sprintf('%s %s of %s %s', $counters[$parts[0]], $daysOfWeek[$parts[1]], $start->format('F'), $start->format('Y'));
599
            $newCarbon = new Carbon($string);
600
            if (0 === $attempts % $skipMod) {
601
                $return[] = clone $newCarbon;
602
            }
603
            $attempts++;
604
            $start->endOfMonth()->addDay();
605
        }
606
607
        return $return;
608
    }
609
610
    /** @noinspection MoreThanThreeArgumentsInspection */
611
    /**
612
     * Get the number of daily occurrences for a recurring transaction until date $end is reached. Will skip every $skipMod-1 occurrences.
613
     *
614
     * @param Carbon $start
615
     * @param Carbon $end
616
     * @param int    $skipMod
617
     * @param string $moment
618
     *
619
     * @return array
620
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
621
     */
622
    private function getWeeklyInRange(Carbon $start, Carbon $end, int $skipMod, string $moment): array
623
    {
624
        $return   = [];
625
        $attempts = 0;
626
        Log::debug('Rep is weekly.');
627
        // monday = 1
628
        // sunday = 7
629
        $dayOfWeek = (int)$moment;
630
        Log::debug(sprintf('DoW in repetition is %d, in mutator is %d', $dayOfWeek, $start->dayOfWeekIso));
631
        if ($start->dayOfWeekIso > $dayOfWeek) {
632
            // day has already passed this week, add one week:
633
            $start->addWeek();
634
            Log::debug(sprintf('Jump to next week, so mutator is now: %s', $start->format('Y-m-d')));
635
        }
636
        // today is wednesday (3), expected is friday (5): add two days.
637
        // today is friday (5), expected is monday (1), subtract four days.
638
        Log::debug(sprintf('Mutator is now: %s', $start->format('Y-m-d')));
639
        $dayDifference = $dayOfWeek - $start->dayOfWeekIso;
640
        $start->addDays($dayDifference);
641
        Log::debug(sprintf('Mutator is now: %s', $start->format('Y-m-d')));
642
        while ($start <= $end) {
643
            if (0 === $attempts % $skipMod && $start->lte($start) && $end->gte($start)) {
644
                Log::debug('Date is in range of start+end, add to set.');
645
                $return[] = clone $start;
646
            }
647
            $attempts++;
648
            $start->addWeek();
649
            Log::debug(sprintf('Mutator is now (end of loop): %s', $start->format('Y-m-d')));
650
        }
651
652
        return $return;
653
    }
654
655
    /**
656
     * Calculates the number of daily occurrences for a recurring transaction, starting at the date, until $count is reached. It will skip
657
     * over $skipMod -1 recurrences.
658
     *
659
     * @param Carbon $date
660
     * @param int    $count
661
     * @param int    $skipMod
662
     *
663
     * @return array
664
     */
665
    private function getXDailyOccurrences(Carbon $date, int $count, int $skipMod): array
666
    {
667
        $return   = [];
668
        $mutator  = clone $date;
669
        $total    = 0;
670
        $attempts = 0;
671
        while ($total < $count) {
672
            $mutator->addDay();
673
            if (0 === $attempts % $skipMod) {
674
                $return[] = clone $mutator;
675
                $total++;
676
            }
677
            $attempts++;
678
        }
679
680
        return $return;
681
    }
682
683
    /** @noinspection MoreThanThreeArgumentsInspection */
684
    /**
685
     * Calculates the number of monthly occurrences for a recurring transaction, starting at the date, until $count is reached. It will skip
686
     * over $skipMod -1 recurrences.
687
     *
688
     * @param Carbon $date
689
     * @param int    $count
690
     * @param int    $skipMod
691
     * @param string $moment
692
     *
693
     * @return array
694
     */
695
    private function getXMonthlyOccurrences(Carbon $date, int $count, int $skipMod, string $moment): array
696
    {
697
        $return   = [];
698
        $mutator  = clone $date;
699
        $total    = 0;
700
        $attempts = 0;
701
        $mutator->addDay(); // always assume today has passed.
702
        $dayOfMonth = (int)$moment;
703
        if ($mutator->day > $dayOfMonth) {
704
            // day has passed already, add a month.
705
            $mutator->addMonth();
706
        }
707
708
        while ($total < $count) {
709
            $domCorrected = min($dayOfMonth, $mutator->daysInMonth);
710
            $mutator->day = $domCorrected;
711
            if (0 === $attempts % $skipMod) {
712
                $return[] = clone $mutator;
713
                $total++;
714
            }
715
            $attempts++;
716
            $mutator->endOfMonth()->addDay();
717
        }
718
719
        return $return;
720
    }
721
722
    /** @noinspection MoreThanThreeArgumentsInspection */
723
    /**
724
     * Calculates the number of NDOM occurrences for a recurring transaction, starting at the date, until $count is reached. It will skip
725
     * over $skipMod -1 recurrences.
726
     *
727
     * @param Carbon $date
728
     * @param int    $count
729
     * @param int    $skipMod
730
     * @param string $moment
731
     *
732
     * @return array
733
     */
734
    private function getXNDomOccurrences(Carbon $date, int $count, int $skipMod, string $moment): array
735
    {
736
        $return   = [];
737
        $total    = 0;
738
        $attempts = 0;
739
        $mutator  = clone $date;
740
        $mutator->addDay(); // always assume today has passed.
741
        $mutator->startOfMonth();
742
        // this feels a bit like a cop out but why reinvent the wheel?
743
        $counters   = [1 => 'first', 2 => 'second', 3 => 'third', 4 => 'fourth', 5 => 'fifth',];
744
        $daysOfWeek = [1 => 'Monday', 2 => 'Tuesday', 3 => 'Wednesday', 4 => 'Thursday', 5 => 'Friday', 6 => 'Saturday', 7 => 'Sunday',];
745
        $parts      = explode(',', $moment);
746
747
        while ($total < $count) {
748
            $string    = sprintf('%s %s of %s %s', $counters[$parts[0]], $daysOfWeek[$parts[1]], $mutator->format('F'), $mutator->format('Y'));
749
            $newCarbon = new Carbon($string);
750
            if (0 === $attempts % $skipMod) {
751
                $return[] = clone $newCarbon;
752
                $total++;
753
            }
754
            $attempts++;
755
            $mutator->endOfMonth()->addDay();
756
        }
757
758
        return $return;
759
    }
760
761
    /** @noinspection MoreThanThreeArgumentsInspection */
762
    /**
763
     * Calculates the number of weekly occurrences for a recurring transaction, starting at the date, until $count is reached. It will skip
764
     * over $skipMod -1 recurrences.
765
     *
766
     * @param Carbon $date
767
     * @param int    $count
768
     * @param int    $skipMod
769
     * @param string $moment
770
     *
771
     * @return array
772
     */
773
    private function getXWeeklyOccurrences(Carbon $date, int $count, int $skipMod, string $moment): array
774
    {
775
        $return   = [];
776
        $total    = 0;
777
        $attempts = 0;
778
        $mutator  = clone $date;
779
        // monday = 1
780
        // sunday = 7
781
        $mutator->addDay(); // always assume today has passed.
782
        $dayOfWeek = (int)$moment;
783
        if ($mutator->dayOfWeekIso > $dayOfWeek) {
784
            // day has already passed this week, add one week:
785
            $mutator->addWeek();
786
        }
787
        // today is wednesday (3), expected is friday (5): add two days.
788
        // today is friday (5), expected is monday (1), subtract four days.
789
        $dayDifference = $dayOfWeek - $mutator->dayOfWeekIso;
790
        $mutator->addDays($dayDifference);
791
792
        while ($total < $count) {
793
            if (0 === $attempts % $skipMod) {
794
                $return[] = clone $mutator;
795
                $total++;
796
            }
797
            $attempts++;
798
            $mutator->addWeek();
799
        }
800
801
        return $return;
802
    }
803
804
    /** @noinspection MoreThanThreeArgumentsInspection */
805
    /**
806
     * Calculates the number of yearly occurrences for a recurring transaction, starting at the date, until $count is reached. It will skip
807
     * over $skipMod -1 recurrences.
808
     *
809
     * @param Carbon $date
810
     * @param int    $count
811
     * @param int    $skipMod
812
     * @param string $moment
813
     *
814
     * @return array
815
     */
816
    private function getXYearlyOccurrences(Carbon $date, int $count, int $skipMod, string $moment): array
817
    {
818
        $return     = [];
819
        $mutator    = clone $date;
820
        $total      = 0;
821
        $attempts   = 0;
822
        $date       = new Carbon($moment);
823
        $date->year = $mutator->year;
824
        if ($mutator > $date) {
825
            $date->addYear();
826
        }
827
        $obj = clone $date;
828
        while ($total < $count) {
829
            if (0 === $attempts % $skipMod) {
830
                $return[] = clone $obj;
831
                $total++;
832
            }
833
            $obj->addYears(1);
834
            $attempts++;
835
        }
836
837
        return $return;
838
839
    }
840
841
    /** @noinspection MoreThanThreeArgumentsInspection */
842
    /**
843
     * Get the number of daily occurrences for a recurring transaction until date $end is reached. Will skip every $skipMod-1 occurrences.
844
     *
845
     * @param Carbon $start
846
     * @param Carbon $end
847
     * @param int    $skipMod
848
     * @param string $moment
849
     *
850
     * @return array
851
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
852
     */
853
    private function getYearlyInRange(Carbon $start, Carbon $end, int $skipMod, string $moment): array
854
    {
855
        $attempts   = 0;
856
        $date       = new Carbon($moment);
857
        $date->year = $start->year;
858
        $return     = [];
859
        if ($start > $date) {
860
            $date->addYear();
861
862
        }
863
864
        // is $date between $start and $end?
865
        $obj   = clone $date;
866
        $count = 0;
867
        while ($obj <= $end && $obj >= $start && $count < 10) {
868
            if (0 === $attempts % $skipMod) {
869
                $return[] = clone $obj;
870
            }
871
            $obj->addYears(1);
872
            $count++;
873
            $attempts++;
874
        }
875
876
        return $return;
877
878
    }
879
}
880