Andrew Wallo преди 9 месеца
родител
ревизия
5177c4b306

+ 39
- 0
app/Console/Commands/TriggerRecurringInvoiceGeneration.php Целия файл

1
+<?php
2
+
3
+namespace App\Console\Commands;
4
+
5
+use App\Jobs\GenerateRecurringInvoices;
6
+use Illuminate\Console\Command;
7
+
8
+class TriggerRecurringInvoiceGeneration extends Command
9
+{
10
+    /**
11
+     * The name and signature of the console command.
12
+     *
13
+     * @var string
14
+     */
15
+    protected $signature = 'invoices:generate-recurring {--queue : Whether the job should be queued}';
16
+
17
+    /**
18
+     * The console command description.
19
+     *
20
+     * @var string
21
+     */
22
+    protected $description = 'Generate invoices for active recurring schedules';
23
+
24
+    /**
25
+     * Execute the console command.
26
+     */
27
+    public function handle(): void
28
+    {
29
+        if ($this->option('queue')) {
30
+            GenerateRecurringInvoices::dispatch();
31
+
32
+            $this->info('Recurring invoice generation has been queued.');
33
+        } else {
34
+            GenerateRecurringInvoices::dispatchSync();
35
+
36
+            $this->info('Recurring invoices have been generated.');
37
+        }
38
+    }
39
+}

+ 21
- 0
app/Enums/Accounting/DayOfMonth.php Целия файл

3
 namespace App\Enums\Accounting;
3
 namespace App\Enums\Accounting;
4
 
4
 
5
 use App\Enums\Concerns\ParsesEnum;
5
 use App\Enums\Concerns\ParsesEnum;
6
+use Carbon\CarbonImmutable;
6
 use Filament\Support\Contracts\HasLabel;
7
 use Filament\Support\Contracts\HasLabel;
8
+use Illuminate\Support\Carbon;
7
 
9
 
8
 enum DayOfMonth: int implements HasLabel
10
 enum DayOfMonth: int implements HasLabel
9
 {
11
 {
79
             self::ThirtyFirst => '31st',
81
             self::ThirtyFirst => '31st',
80
         };
82
         };
81
     }
83
     }
84
+
85
+    public function isFirst(): bool
86
+    {
87
+        return $this === self::First;
88
+    }
89
+
90
+    public function isLast(): bool
91
+    {
92
+        return $this === self::Last;
93
+    }
94
+
95
+    public function resolveDate(Carbon | CarbonImmutable $date): Carbon | CarbonImmutable
96
+    {
97
+        if ($this->isLast()) {
98
+            return $date->endOfMonth();
99
+        }
100
+
101
+        return $date->day(min($this->value, $date->daysInMonth));
102
+    }
82
 }
103
 }

+ 18
- 2
app/Filament/Company/Resources/Sales/RecurringInvoiceResource/Pages/ViewRecurringInvoice.php Целия файл

4
 
4
 
5
 use App\Enums\Accounting\DocumentType;
5
 use App\Enums\Accounting\DocumentType;
6
 use App\Filament\Company\Resources\Sales\ClientResource;
6
 use App\Filament\Company\Resources\Sales\ClientResource;
7
+use App\Filament\Company\Resources\Sales\InvoiceResource;
7
 use App\Filament\Company\Resources\Sales\RecurringInvoiceResource;
8
 use App\Filament\Company\Resources\Sales\RecurringInvoiceResource;
8
 use App\Filament\Infolists\Components\DocumentPreview;
9
 use App\Filament\Infolists\Components\DocumentPreview;
9
 use App\Models\Accounting\RecurringInvoice;
10
 use App\Models\Accounting\RecurringInvoice;
19
 use Filament\Support\Enums\IconPosition;
20
 use Filament\Support\Enums\IconPosition;
20
 use Filament\Support\Enums\IconSize;
21
 use Filament\Support\Enums\IconSize;
21
 use Filament\Support\Enums\MaxWidth;
22
 use Filament\Support\Enums\MaxWidth;
23
+use Illuminate\Support\Str;
22
 
24
 
23
 class ViewRecurringInvoice extends ViewRecord
25
 class ViewRecurringInvoice extends ViewRecord
24
 {
26
 {
62
                         RecurringInvoice::getUpdateScheduleAction(Action::class)
64
                         RecurringInvoice::getUpdateScheduleAction(Action::class)
63
                             ->outlined(),
65
                             ->outlined(),
64
                     ]),
66
                     ]),
67
+                SimpleAlert::make('readyToApprove')
68
+                    ->info()
69
+                    ->title('Ready to Approve')
70
+                    ->description('This recurring invoice is ready for approval. Review the details, and approve it when you’re ready to start generating invoices.')
71
+                    ->visible(fn (RecurringInvoice $record) => $record->isDraft() && $record->hasSchedule())
72
+                    ->columnSpanFull()
73
+                    ->actions([
74
+                        RecurringInvoice::getApproveDraftAction(Action::class)
75
+                            ->outlined(),
76
+                    ]),
65
                 Section::make('Invoice Details')
77
                 Section::make('Invoice Details')
66
                     ->columns(4)
78
                     ->columns(4)
67
                     ->schema([
79
                     ->schema([
91
                                         return $record->getTimelineDescription();
103
                                         return $record->getTimelineDescription();
92
                                     }),
104
                                     }),
93
                                 TextEntry::make('occurrences_count')
105
                                 TextEntry::make('occurrences_count')
94
-                                    ->label('Invoices Created')
95
-                                    ->visible(fn (RecurringInvoice $record) => $record->occurrences_count > 0),
106
+                                    ->label('Created to Date')
107
+                                    ->visible(static fn (RecurringInvoice $record) => $record->occurrences_count > 0)
108
+                                    ->color('primary')
109
+                                    ->weight(FontWeight::SemiBold)
110
+                                    ->suffix(fn (RecurringInvoice $record) => Str::of(' invoice')->plural($record->occurrences_count))
111
+                                    ->url(static fn (RecurringInvoice $record) => InvoiceResource::getUrl()),
96
                                 TextEntry::make('end_date')
112
                                 TextEntry::make('end_date')
97
                                     ->label('Ends On')
113
                                     ->label('Ends On')
98
                                     ->date()
114
                                     ->date()

+ 33
- 0
app/Jobs/GenerateRecurringInvoices.php Целия файл

1
+<?php
2
+
3
+namespace App\Jobs;
4
+
5
+use App\Enums\Accounting\RecurringInvoiceStatus;
6
+use App\Models\Accounting\RecurringInvoice;
7
+use Illuminate\Contracts\Queue\ShouldQueue;
8
+use Illuminate\Foundation\Bus\Dispatchable;
9
+use Illuminate\Foundation\Queue\Queueable;
10
+use Illuminate\Queue\InteractsWithQueue;
11
+use Illuminate\Queue\SerializesModels;
12
+
13
+class GenerateRecurringInvoices implements ShouldQueue
14
+{
15
+    use Dispatchable;
16
+    use InteractsWithQueue;
17
+    use Queueable;
18
+    use SerializesModels;
19
+
20
+    /**
21
+     * Execute the job.
22
+     */
23
+    public function handle(): void
24
+    {
25
+        RecurringInvoice::query()
26
+            ->where('status', RecurringInvoiceStatus::Active)
27
+            ->chunk(100, function ($recurringInvoices) {
28
+                foreach ($recurringInvoices as $recurringInvoice) {
29
+                    $recurringInvoice->generateDueInvoices();
30
+                }
31
+            });
32
+    }
33
+}

+ 269
- 43
app/Models/Accounting/RecurringInvoice.php Целия файл

13
 use App\Enums\Accounting\EndType;
13
 use App\Enums\Accounting\EndType;
14
 use App\Enums\Accounting\Frequency;
14
 use App\Enums\Accounting\Frequency;
15
 use App\Enums\Accounting\IntervalType;
15
 use App\Enums\Accounting\IntervalType;
16
+use App\Enums\Accounting\InvoiceStatus;
16
 use App\Enums\Accounting\Month;
17
 use App\Enums\Accounting\Month;
17
 use App\Enums\Accounting\RecurringInvoiceStatus;
18
 use App\Enums\Accounting\RecurringInvoiceStatus;
18
 use App\Enums\Setting\PaymentTerms;
19
 use App\Enums\Setting\PaymentTerms;
28
 use Guava\FilamentClusters\Forms\Cluster;
29
 use Guava\FilamentClusters\Forms\Cluster;
29
 use Illuminate\Database\Eloquent\Attributes\CollectedBy;
30
 use Illuminate\Database\Eloquent\Attributes\CollectedBy;
30
 use Illuminate\Database\Eloquent\Attributes\ObservedBy;
31
 use Illuminate\Database\Eloquent\Attributes\ObservedBy;
32
+use Illuminate\Database\Eloquent\Model;
31
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
33
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
32
 use Illuminate\Database\Eloquent\Relations\HasMany;
34
 use Illuminate\Database\Eloquent\Relations\HasMany;
33
 use Illuminate\Support\Carbon;
35
 use Illuminate\Support\Carbon;
91
         'payment_terms' => PaymentTerms::class,
93
         'payment_terms' => PaymentTerms::class,
92
         'frequency' => Frequency::class,
94
         'frequency' => Frequency::class,
93
         'interval_type' => IntervalType::class,
95
         'interval_type' => IntervalType::class,
96
+        'interval_value' => 'integer',
94
         'month' => Month::class,
97
         'month' => Month::class,
95
         'day_of_month' => DayOfMonth::class,
98
         'day_of_month' => DayOfMonth::class,
96
         'day_of_week' => DayOfWeek::class,
99
         'day_of_week' => DayOfWeek::class,
223
         return "Repeat every {$interval}{$dayDescription}";
226
         return "Repeat every {$interval}{$dayDescription}";
224
     }
227
     }
225
 
228
 
226
-    /**
227
-     * Get a human-readable description of when the schedule ends.
228
-     */
229
     public function getEndDescription(): string
229
     public function getEndDescription(): string
230
     {
230
     {
231
         if (! $this->end_type) {
231
         if (! $this->end_type) {
243
         };
243
         };
244
     }
244
     }
245
 
245
 
246
-    /**
247
-     * Get the schedule timeline description.
248
-     */
249
     public function getTimelineDescription(): string
246
     public function getTimelineDescription(): string
250
     {
247
     {
251
         $parts = [];
248
         $parts = [];
261
         return implode(', ', $parts);
258
         return implode(', ', $parts);
262
     }
259
     }
263
 
260
 
264
-    /**
265
-     * Get next occurrence date based on the schedule.
266
-     */
267
-    public function calculateNextDate(): ?Carbon
261
+    public function calculateNextDate(?Carbon $lastDate = null): ?Carbon
268
     {
262
     {
269
-        $lastDate = $this->last_date ?? $this->start_date;
263
+        $lastDate ??= $this->last_date;
264
+
265
+        if (! $lastDate && $this->start_date) {
266
+            return $this->start_date;
267
+        }
268
+
270
         if (! $lastDate) {
269
         if (! $lastDate) {
271
             return null;
270
             return null;
272
         }
271
         }
274
         $nextDate = match (true) {
273
         $nextDate = match (true) {
275
             $this->frequency->isDaily() => $lastDate->addDay(),
274
             $this->frequency->isDaily() => $lastDate->addDay(),
276
 
275
 
277
-            $this->frequency->isWeekly() => $lastDate->addWeek(),
276
+            $this->frequency->isWeekly() => $this->calculateNextWeeklyDate($lastDate),
278
 
277
 
279
-            $this->frequency->isMonthly() => $lastDate->addMonth(),
278
+            $this->frequency->isMonthly() => $this->calculateNextMonthlyDate($lastDate),
280
 
279
 
281
-            $this->frequency->isYearly() => $lastDate->addYear(),
280
+            $this->frequency->isYearly() => $this->calculateNextYearlyDate($lastDate),
282
 
281
 
283
             $this->frequency->isCustom() => $this->calculateCustomNextDate($lastDate),
282
             $this->frequency->isCustom() => $this->calculateCustomNextDate($lastDate),
284
 
283
 
285
             default => null
284
             default => null
286
         };
285
         };
287
 
286
 
288
-        // Check if we've reached the end
289
-        if ($this->hasReachedEnd($nextDate)) {
287
+        if (! $nextDate || $this->hasReachedEnd($nextDate)) {
290
             return null;
288
             return null;
291
         }
289
         }
292
 
290
 
293
         return $nextDate;
291
         return $nextDate;
294
     }
292
     }
295
 
293
 
296
-    public function calculateNextDueDate(): ?Carbon
294
+    public function calculateNextWeeklyDate(Carbon $lastDate): ?Carbon
297
     {
295
     {
298
-        $nextDate = $this->calculateNextDate();
299
-        if (! $nextDate) {
300
-            return null;
301
-        }
296
+        return $lastDate->copy()->next($this->day_of_week->value);
297
+    }
302
 
298
 
303
-        $terms = $this->payment_terms;
304
-        if (! $terms) {
305
-            return $nextDate;
306
-        }
299
+    public function calculateNextMonthlyDate(Carbon $lastDate): ?Carbon
300
+    {
301
+        return match (true) {
302
+            $lastDate->equalTo($this->start_date) => $lastDate->copy()->day(
303
+                min($this->day_of_month->value, $lastDate->daysInMonth)
304
+            ),
307
 
305
 
308
-        return $nextDate->addDays($terms->getDays());
306
+            default => $lastDate->copy()->addMonth()->day(
307
+                min($this->day_of_month->value, $lastDate->copy()->addMonth()->daysInMonth)
308
+            ),
309
+        };
309
     }
310
     }
310
 
311
 
311
-    /**
312
-     * Calculate next date for custom intervals
313
-     */
314
-    protected function calculateCustomNextDate(Carbon $lastDate): ?\Carbon\Carbon
312
+    public function calculateNextYearlyDate(Carbon $lastDate): ?Carbon
315
     {
313
     {
316
-        $value = $this->interval_value ?? 1;
314
+        return match (true) {
315
+            $lastDate->equalTo($this->start_date) => $lastDate->copy()
316
+                ->month($this->month->value)
317
+                ->day(min($this->day_of_month->value, $lastDate->daysInMonth)),
318
+
319
+            default => $lastDate->copy()
320
+                ->addYear()
321
+                ->month($this->month->value)
322
+                ->day(min($this->day_of_month->value, $lastDate->copy()->addYear()->month($this->month->value)->daysInMonth))
323
+        };
324
+    }
325
+
326
+    protected function calculateCustomNextDate(Carbon $lastDate): ?Carbon
327
+    {
328
+        $interval = $this->interval_value ?? 1;
317
 
329
 
318
         return match ($this->interval_type) {
330
         return match ($this->interval_type) {
319
-            IntervalType::Day => $lastDate->addDays($value),
320
-            IntervalType::Week => $lastDate->addWeeks($value),
321
-            IntervalType::Month => $lastDate->addMonths($value),
322
-            IntervalType::Year => $lastDate->addYears($value),
331
+            IntervalType::Day => $lastDate->copy()->addDays($interval),
332
+
333
+            IntervalType::Week => match (true) {
334
+                $lastDate->equalTo($this->start_date) => $lastDate->copy()->next($this->day_of_week->value),
335
+
336
+                $lastDate->dayOfWeek === $this->day_of_week->value => $lastDate->copy()->addWeeks($interval),
337
+
338
+                default => $lastDate->copy()->next($this->day_of_week->value),
339
+            },
340
+
341
+            IntervalType::Month => match (true) {
342
+                $lastDate->equalTo($this->start_date) => $lastDate->copy()->day(
343
+                    min($this->day_of_month->value, $lastDate->daysInMonth)
344
+                ),
345
+
346
+                default => $lastDate->copy()->addMonths($interval)->day(
347
+                    min($this->day_of_month->value, $lastDate->copy()->addMonths($interval)->daysInMonth)
348
+                ),
349
+            },
350
+
351
+            IntervalType::Year => match (true) {
352
+                $lastDate->equalTo($this->start_date) => $lastDate->copy()
353
+                    ->month($this->month->value)
354
+                    ->day(min($this->day_of_month->value, $lastDate->daysInMonth)),
355
+
356
+                default => $lastDate->copy()
357
+                    ->addYears($interval)
358
+                    ->month($this->month->value)
359
+                    ->day(min($this->day_of_month->value, $lastDate->copy()->addYears($interval)->month($this->month->value)->daysInMonth))
360
+            },
361
+
323
             default => null
362
             default => null
324
         };
363
         };
325
     }
364
     }
326
 
365
 
327
-    /**
328
-     * Check if the schedule has reached its end
329
-     */
366
+    public function calculateNextDueDate(): ?Carbon
367
+    {
368
+        if (! $nextDate = $this->calculateNextDate()) {
369
+            return null;
370
+        }
371
+
372
+        if (! $terms = $this->payment_terms) {
373
+            return $nextDate;
374
+        }
375
+
376
+        return $nextDate->copy()->addDays($terms->getDays());
377
+    }
378
+
330
     public function hasReachedEnd(?Carbon $nextDate = null): bool
379
     public function hasReachedEnd(?Carbon $nextDate = null): bool
331
     {
380
     {
332
         if (! $this->end_type) {
381
         if (! $this->end_type) {
464
                             ->visible(
513
                             ->visible(
465
                                 fn (Forms\Get $get) => Frequency::parse($get('frequency'))->isYearly() ||
514
                                 fn (Forms\Get $get) => Frequency::parse($get('frequency'))->isYearly() ||
466
                                 IntervalType::parse($get('interval_type'))?->isYear()
515
                                 IntervalType::parse($get('interval_type'))?->isYear()
467
-                            ),
516
+                            )
517
+                            ->live()
518
+                            ->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state) {
519
+                                $dayOfMonth = DayOfMonth::parse($get('day_of_month'));
520
+                                $frequency = Frequency::parse($get('frequency'));
521
+                                $intervalType = IntervalType::parse($get('interval_type'));
522
+                                $month = Month::parse($state);
523
+
524
+                                if (($frequency->isYearly() || $intervalType?->isYear()) && $month && $dayOfMonth) {
525
+                                    $date = $dayOfMonth->resolveDate(today()->month($month->value))->toImmutable();
526
+
527
+                                    $adjustedStartDate = $date->lt(today())
528
+                                        ? $dayOfMonth->resolveDate($date->addYear()->month($month->value))
529
+                                        : $dayOfMonth->resolveDate($date->month($month->value));
530
+
531
+                                    $adjustedDay = min($dayOfMonth->value, $adjustedStartDate->daysInMonth);
532
+
533
+                                    $set('day_of_month', $adjustedDay);
534
+
535
+                                    $set('start_date', $adjustedStartDate);
536
+                                }
537
+                            }),
468
 
538
 
469
                         Forms\Components\Select::make('day_of_month')
539
                         Forms\Components\Select::make('day_of_month')
470
                             ->label('Day of Month')
540
                             ->label('Day of Month')
471
-                            ->options(DayOfMonth::class)
541
+                            ->options(function (Forms\Get $get) {
542
+                                $month = Month::parse($get('month')) ?? Month::January;
543
+
544
+                                $daysInMonth = Carbon::createFromDate(null, $month->value)->daysInMonth;
545
+
546
+                                return collect(DayOfMonth::cases())
547
+                                    ->filter(static fn (DayOfMonth $dayOfMonth) => $dayOfMonth->value <= $daysInMonth || $dayOfMonth->isLast())
548
+                                    ->mapWithKeys(fn (DayOfMonth $dayOfMonth) => [$dayOfMonth->value => $dayOfMonth->getLabel()]);
549
+                            })
472
                             ->softRequired()
550
                             ->softRequired()
473
                             ->visible(
551
                             ->visible(
474
                                 fn (Forms\Get $get) => Frequency::parse($get('frequency'))?->isMonthly() ||
552
                                 fn (Forms\Get $get) => Frequency::parse($get('frequency'))?->isMonthly() ||
475
                                 Frequency::parse($get('frequency'))?->isYearly() ||
553
                                 Frequency::parse($get('frequency'))?->isYearly() ||
476
                                 IntervalType::parse($get('interval_type'))?->isMonth() ||
554
                                 IntervalType::parse($get('interval_type'))?->isMonth() ||
477
                                 IntervalType::parse($get('interval_type'))?->isYear()
555
                                 IntervalType::parse($get('interval_type'))?->isYear()
478
-                            ),
556
+                            )
557
+                            ->live()
558
+                            ->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state) {
559
+                                $dayOfMonth = DayOfMonth::parse($state);
560
+                                $frequency = Frequency::parse($get('frequency'));
561
+                                $intervalType = IntervalType::parse($get('interval_type'));
562
+                                $month = Month::parse($get('month'));
563
+
564
+                                if (($frequency->isMonthly() || $intervalType?->isMonth()) && $dayOfMonth) {
565
+                                    $date = $dayOfMonth->resolveDate(today())->toImmutable();
566
+
567
+                                    $adjustedStartDate = $date->lt(today())
568
+                                        ? $dayOfMonth->resolveDate($date->addMonth())
569
+                                        : $dayOfMonth->resolveDate($date);
570
+
571
+                                    $set('start_date', $adjustedStartDate);
572
+                                }
573
+
574
+                                if (($frequency->isYearly() || $intervalType?->isYear()) && $month && $dayOfMonth) {
575
+                                    $date = $dayOfMonth->resolveDate(today()->month($month->value))->toImmutable();
576
+
577
+                                    $adjustedStartDate = $date->lt(today())
578
+                                        ? $dayOfMonth->resolveDate($date->addYear()->month($month->value))
579
+                                        : $dayOfMonth->resolveDate($date->month($month->value));
580
+
581
+                                    $set('start_date', $adjustedStartDate);
582
+                                }
583
+                            }),
479
 
584
 
480
                         Forms\Components\Select::make('day_of_week')
585
                         Forms\Components\Select::make('day_of_week')
481
                             ->label('Day of Week')
586
                             ->label('Day of Week')
484
                             ->visible(
589
                             ->visible(
485
                                 fn (Forms\Get $get) => Frequency::parse($get('frequency'))?->isWeekly() ||
590
                                 fn (Forms\Get $get) => Frequency::parse($get('frequency'))?->isWeekly() ||
486
                                 IntervalType::parse($get('interval_type'))?->isWeek()
591
                                 IntervalType::parse($get('interval_type'))?->isWeek()
487
-                            ),
592
+                            )
593
+                            ->live()
594
+                            ->afterStateUpdated(function (Forms\Set $set, $state) {
595
+                                $dayOfWeek = DayOfWeek::parse($state);
596
+
597
+                                $adjustedStartDate = today()->is($dayOfWeek->name)
598
+                                    ? today()
599
+                                    : today()->next($dayOfWeek->name);
600
+
601
+                                $set('start_date', $adjustedStartDate);
602
+                            }),
488
                     ])->columns(2),
603
                     ])->columns(2),
489
 
604
 
490
                 CustomSection::make('Dates & Time')
605
                 CustomSection::make('Dates & Time')
492
                     ->schema([
607
                     ->schema([
493
                         Forms\Components\DatePicker::make('start_date')
608
                         Forms\Components\DatePicker::make('start_date')
494
                             ->label('First Invoice Date')
609
                             ->label('First Invoice Date')
495
-                            ->softRequired(),
610
+                            ->softRequired()
611
+                            ->live()
612
+                            ->minDate(today())
613
+                            ->closeOnDateSelection()
614
+                            ->afterStateUpdated(function (Forms\Set $set, $state) {
615
+                                $startDate = Carbon::parse($state);
616
+
617
+                                $dayOfWeek = DayOfWeek::parse($startDate->dayOfWeek);
618
+
619
+                                $set('day_of_week', $dayOfWeek);
620
+                            }),
496
 
621
 
497
                         Forms\Components\Group::make(function (Forms\Get $get) {
622
                         Forms\Components\Group::make(function (Forms\Get $get) {
498
                             $components = [];
623
                             $components = [];
587
             'status' => RecurringInvoiceStatus::Active,
712
             'status' => RecurringInvoiceStatus::Active,
588
         ]);
713
         ]);
589
     }
714
     }
715
+
716
+    public function generateInvoice(): ?Invoice
717
+    {
718
+        if (! $this->shouldGenerateInvoice()) {
719
+            return null;
720
+        }
721
+
722
+        $nextDate = $this->next_date ?? $this->calculateNextDate();
723
+
724
+        if (! $nextDate) {
725
+            return null;
726
+        }
727
+
728
+        $dueDate = $this->calculateNextDueDate();
729
+
730
+        $invoice = $this->invoices()->create([
731
+            'company_id' => $this->company_id,
732
+            'client_id' => $this->client_id,
733
+            'logo' => $this->logo,
734
+            'header' => $this->header,
735
+            'subheader' => $this->subheader,
736
+            'invoice_number' => Invoice::getNextDocumentNumber($this->company),
737
+            'date' => $nextDate,
738
+            'due_date' => $dueDate,
739
+            'status' => InvoiceStatus::Draft,
740
+            'currency_code' => $this->currency_code,
741
+            'discount_method' => $this->discount_method,
742
+            'discount_computation' => $this->discount_computation,
743
+            'discount_rate' => $this->discount_rate,
744
+            'subtotal' => $this->subtotal,
745
+            'tax_total' => $this->tax_total,
746
+            'discount_total' => $this->discount_total,
747
+            'total' => $this->total,
748
+            'terms' => $this->terms,
749
+            'footer' => $this->footer,
750
+            'created_by' => auth()->id(),
751
+            'updated_by' => auth()->id(),
752
+        ]);
753
+
754
+        $this->replicateLineItems($invoice);
755
+
756
+        $this->update([
757
+            'last_date' => $nextDate,
758
+            'next_date' => $this->calculateNextDate($nextDate),
759
+            'occurrences_count' => ($this->occurrences_count ?? 0) + 1,
760
+        ]);
761
+
762
+        return $invoice;
763
+    }
764
+
765
+    public function replicateLineItems(Model $target): void
766
+    {
767
+        $this->lineItems->each(function (DocumentLineItem $lineItem) use ($target) {
768
+            $replica = $lineItem->replicate([
769
+                'documentable_id',
770
+                'documentable_type',
771
+                'subtotal',
772
+                'total',
773
+                'created_by',
774
+                'updated_by',
775
+                'created_at',
776
+                'updated_at',
777
+            ]);
778
+
779
+            $replica->documentable_id = $target->id;
780
+            $replica->documentable_type = $target->getMorphClass();
781
+            $replica->save();
782
+
783
+            $replica->adjustments()->sync($lineItem->adjustments->pluck('id'));
784
+        });
785
+    }
786
+
787
+    public function shouldGenerateInvoice(): bool
788
+    {
789
+        if (! $this->isActive() || $this->hasReachedEnd()) {
790
+            return false;
791
+        }
792
+
793
+        $nextDate = $this->calculateNextDate();
794
+
795
+        if (! $nextDate || $nextDate->startOfDay()->isFuture()) {
796
+            return false;
797
+        }
798
+
799
+        return true;
800
+    }
801
+
802
+    public function generateDueInvoices(): void
803
+    {
804
+        $maxIterations = 100;
805
+
806
+        for ($i = 0; $i < $maxIterations; $i++) {
807
+            $result = $this->generateInvoice();
808
+
809
+            if (! $result) {
810
+                break;
811
+            }
812
+
813
+            $this->refresh();
814
+        }
815
+    }
590
 }
816
 }

+ 38
- 10
app/Observers/RecurringInvoiceObserver.php Целия файл

2
 
2
 
3
 namespace App\Observers;
3
 namespace App\Observers;
4
 
4
 
5
+use App\Enums\Accounting\RecurringInvoiceStatus;
6
+use App\Models\Accounting\DocumentLineItem;
5
 use App\Models\Accounting\RecurringInvoice;
7
 use App\Models\Accounting\RecurringInvoice;
8
+use Illuminate\Support\Facades\DB;
6
 
9
 
7
 class RecurringInvoiceObserver
10
 class RecurringInvoiceObserver
8
 {
11
 {
9
-    /**
10
-     * Handle the RecurringInvoice "updated" event.
11
-     */
12
-    public function updated(RecurringInvoice $recurringInvoice): void
12
+    public function saving(RecurringInvoice $recurringInvoice): void
13
     {
13
     {
14
-        $recurringInvoice->updateQuietly([
15
-            'next_date' => $recurringInvoice->calculateNextDate(),
14
+        if (($recurringInvoice->isDirty('start_date') && ! $recurringInvoice->last_date) || $this->otherScheduleDetailsChanged($recurringInvoice)) {
15
+            $recurringInvoice->next_date = $recurringInvoice->calculateNextDate();
16
+        }
17
+
18
+        if ($recurringInvoice->end_type?->isAfter() && $recurringInvoice->occurrences_count >= $recurringInvoice->max_occurrences) {
19
+            $recurringInvoice->status = RecurringInvoiceStatus::Ended;
20
+            $recurringInvoice->ended_at = now();
21
+        }
22
+    }
23
+
24
+    public function saved(RecurringInvoice $recurringInvoice): void
25
+    {
26
+        if ($recurringInvoice->wasChanged('status')) {
27
+            $recurringInvoice->generateDueInvoices();
28
+        }
29
+    }
30
+
31
+    protected function otherScheduleDetailsChanged(RecurringInvoice $recurringInvoice): bool
32
+    {
33
+        return $recurringInvoice->isDirty([
34
+            'frequency',
35
+            'interval_type',
36
+            'interval_value',
37
+            'month',
38
+            'day_of_month',
39
+            'day_of_week',
40
+            'end_type',
41
+            'max_occurrences',
42
+            'end_date',
16
         ]);
43
         ]);
17
     }
44
     }
18
 
45
 
19
-    /**
20
-     * Handle the RecurringInvoice "deleted" event.
21
-     */
22
     public function deleted(RecurringInvoice $recurringInvoice): void
46
     public function deleted(RecurringInvoice $recurringInvoice): void
23
     {
47
     {
24
-        //
48
+        DB::transaction(function () use ($recurringInvoice) {
49
+            $recurringInvoice->lineItems()->each(function (DocumentLineItem $lineItem) {
50
+                $lineItem->delete();
51
+            });
52
+        });
25
     }
53
     }
26
 }
54
 }

+ 96
- 2
database/factories/Accounting/RecurringInvoiceFactory.php Целия файл

2
 
2
 
3
 namespace Database\Factories\Accounting;
3
 namespace Database\Factories\Accounting;
4
 
4
 
5
+use App\Enums\Accounting\DayOfMonth;
6
+use App\Enums\Accounting\DayOfWeek;
7
+use App\Enums\Accounting\EndType;
8
+use App\Enums\Accounting\Frequency;
9
+use App\Enums\Accounting\IntervalType;
10
+use App\Enums\Accounting\Month;
11
+use App\Enums\Accounting\RecurringInvoiceStatus;
12
+use App\Enums\Setting\PaymentTerms;
13
+use App\Models\Accounting\RecurringInvoice;
14
+use App\Models\Common\Client;
5
 use Illuminate\Database\Eloquent\Factories\Factory;
15
 use Illuminate\Database\Eloquent\Factories\Factory;
16
+use Illuminate\Support\Carbon;
6
 
17
 
7
 /**
18
 /**
8
- * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Accounting\RecurringInvoice>
19
+ * @extends Factory<RecurringInvoice>
9
  */
20
  */
10
 class RecurringInvoiceFactory extends Factory
21
 class RecurringInvoiceFactory extends Factory
11
 {
22
 {
23
+    /**
24
+     * The name of the factory's corresponding model.
25
+     */
26
+    protected $model = RecurringInvoice::class;
27
+
12
     /**
28
     /**
13
      * Define the model's default state.
29
      * Define the model's default state.
14
      *
30
      *
17
     public function definition(): array
33
     public function definition(): array
18
     {
34
     {
19
         return [
35
         return [
20
-            //
36
+            'company_id' => 1,
37
+            'client_id' => Client::inRandomOrder()->value('id'),
38
+            'status' => RecurringInvoiceStatus::Draft,
39
+
40
+            // Schedule configuration
41
+            'frequency' => Frequency::Monthly,
42
+            'day_of_month' => DayOfMonth::First,
43
+
44
+            // Date configuration
45
+            'start_date' => now()->addMonth()->startOfMonth(),
46
+            'end_type' => EndType::Never,
47
+
48
+            // Invoice configuration
49
+            'payment_terms' => PaymentTerms::DueUponReceipt,
50
+            'currency_code' => 'USD',
51
+
52
+            // Timestamps and user tracking
53
+            'terms' => $this->faker->sentence,
54
+            'footer' => $this->faker->sentence,
55
+            'created_by' => 1,
56
+            'updated_by' => 1,
21
         ];
57
         ];
22
     }
58
     }
59
+
60
+    public function weekly(DayOfWeek $dayOfWeek = DayOfWeek::Monday): self
61
+    {
62
+        return $this->state([
63
+            'frequency' => Frequency::Weekly,
64
+            'day_of_week' => $dayOfWeek,
65
+        ]);
66
+    }
67
+
68
+    public function monthly(DayOfMonth $dayOfMonth = DayOfMonth::First): self
69
+    {
70
+        return $this->state([
71
+            'frequency' => Frequency::Monthly,
72
+            'day_of_month' => $dayOfMonth,
73
+        ]);
74
+    }
75
+
76
+    public function yearly(Month $month = Month::January, DayOfMonth $dayOfMonth = DayOfMonth::First): self
77
+    {
78
+        return $this->state([
79
+            'frequency' => Frequency::Yearly,
80
+            'month' => $month,
81
+            'day_of_month' => $dayOfMonth,
82
+        ]);
83
+    }
84
+
85
+    public function custom(IntervalType $intervalType, int $intervalValue = 1): self
86
+    {
87
+        return $this->state([
88
+            'frequency' => Frequency::Custom,
89
+            'interval_type' => $intervalType,
90
+            'interval_value' => $intervalValue,
91
+        ]);
92
+    }
93
+
94
+    public function withEndDate(Carbon $endDate): self
95
+    {
96
+        return $this->state([
97
+            'end_type' => EndType::On,
98
+            'end_date' => $endDate,
99
+        ]);
100
+    }
101
+
102
+    public function withMaxOccurrences(int $maxOccurrences): self
103
+    {
104
+        return $this->state([
105
+            'end_type' => EndType::After,
106
+            'max_occurrences' => $maxOccurrences,
107
+        ]);
108
+    }
109
+
110
+    public function autoSend(string $time = '09:00'): self
111
+    {
112
+        return $this->state([
113
+            'auto_send' => true,
114
+            'send_time' => $time,
115
+        ]);
116
+    }
23
 }
117
 }

+ 83
- 83
package-lock.json Целия файл

575
             }
575
             }
576
         },
576
         },
577
         "node_modules/@rollup/rollup-android-arm-eabi": {
577
         "node_modules/@rollup/rollup-android-arm-eabi": {
578
-            "version": "4.29.1",
579
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.29.1.tgz",
580
-            "integrity": "sha512-ssKhA8RNltTZLpG6/QNkCSge+7mBQGUqJRisZ2MDQcEGaK93QESEgWK2iOpIDZ7k9zPVkG5AS3ksvD5ZWxmItw==",
578
+            "version": "4.29.2",
579
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.29.2.tgz",
580
+            "integrity": "sha512-s/8RiF4bdmGnc/J0N7lHAr5ZFJj+NdJqJ/Hj29K+c4lEdoVlukzvWXB9XpWZCdakVT0YAw8iyIqUP2iFRz5/jA==",
581
             "cpu": [
581
             "cpu": [
582
                 "arm"
582
                 "arm"
583
             ],
583
             ],
589
             ]
589
             ]
590
         },
590
         },
591
         "node_modules/@rollup/rollup-android-arm64": {
591
         "node_modules/@rollup/rollup-android-arm64": {
592
-            "version": "4.29.1",
593
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.29.1.tgz",
594
-            "integrity": "sha512-CaRfrV0cd+NIIcVVN/jx+hVLN+VRqnuzLRmfmlzpOzB87ajixsN/+9L5xNmkaUUvEbI5BmIKS+XTwXsHEb65Ew==",
592
+            "version": "4.29.2",
593
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.29.2.tgz",
594
+            "integrity": "sha512-mKRlVj1KsKWyEOwR6nwpmzakq6SgZXW4NUHNWlYSiyncJpuXk7wdLzuKdWsRoR1WLbWsZBKvsUCdCTIAqRn9cA==",
595
             "cpu": [
595
             "cpu": [
596
                 "arm64"
596
                 "arm64"
597
             ],
597
             ],
603
             ]
603
             ]
604
         },
604
         },
605
         "node_modules/@rollup/rollup-darwin-arm64": {
605
         "node_modules/@rollup/rollup-darwin-arm64": {
606
-            "version": "4.29.1",
607
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.29.1.tgz",
608
-            "integrity": "sha512-2ORr7T31Y0Mnk6qNuwtyNmy14MunTAMx06VAPI6/Ju52W10zk1i7i5U3vlDRWjhOI5quBcrvhkCHyF76bI7kEw==",
606
+            "version": "4.29.2",
607
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.29.2.tgz",
608
+            "integrity": "sha512-vJX+vennGwygmutk7N333lvQ/yKVAHnGoBS2xMRQgXWW8tvn46YWuTDOpKroSPR9BEW0Gqdga2DHqz8Pwk6X5w==",
609
             "cpu": [
609
             "cpu": [
610
                 "arm64"
610
                 "arm64"
611
             ],
611
             ],
617
             ]
617
             ]
618
         },
618
         },
619
         "node_modules/@rollup/rollup-darwin-x64": {
619
         "node_modules/@rollup/rollup-darwin-x64": {
620
-            "version": "4.29.1",
621
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.29.1.tgz",
622
-            "integrity": "sha512-j/Ej1oanzPjmN0tirRd5K2/nncAhS9W6ICzgxV+9Y5ZsP0hiGhHJXZ2JQ53iSSjj8m6cRY6oB1GMzNn2EUt6Ng==",
620
+            "version": "4.29.2",
621
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.29.2.tgz",
622
+            "integrity": "sha512-e2rW9ng5O6+Mt3ht8fH0ljfjgSCC6ffmOipiLUgAnlK86CHIaiCdHCzHzmTkMj6vEkqAiRJ7ss6Ibn56B+RE5w==",
623
             "cpu": [
623
             "cpu": [
624
                 "x64"
624
                 "x64"
625
             ],
625
             ],
631
             ]
631
             ]
632
         },
632
         },
633
         "node_modules/@rollup/rollup-freebsd-arm64": {
633
         "node_modules/@rollup/rollup-freebsd-arm64": {
634
-            "version": "4.29.1",
635
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.29.1.tgz",
636
-            "integrity": "sha512-91C//G6Dm/cv724tpt7nTyP+JdN12iqeXGFM1SqnljCmi5yTXriH7B1r8AD9dAZByHpKAumqP1Qy2vVNIdLZqw==",
634
+            "version": "4.29.2",
635
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.29.2.tgz",
636
+            "integrity": "sha512-/xdNwZe+KesG6XJCK043EjEDZTacCtL4yurMZRLESIgHQdvtNyul3iz2Ab03ZJG0pQKbFTu681i+4ETMF9uE/Q==",
637
             "cpu": [
637
             "cpu": [
638
                 "arm64"
638
                 "arm64"
639
             ],
639
             ],
645
             ]
645
             ]
646
         },
646
         },
647
         "node_modules/@rollup/rollup-freebsd-x64": {
647
         "node_modules/@rollup/rollup-freebsd-x64": {
648
-            "version": "4.29.1",
649
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.29.1.tgz",
650
-            "integrity": "sha512-hEioiEQ9Dec2nIRoeHUP6hr1PSkXzQaCUyqBDQ9I9ik4gCXQZjJMIVzoNLBRGet+hIUb3CISMh9KXuCcWVW/8w==",
648
+            "version": "4.29.2",
649
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.29.2.tgz",
650
+            "integrity": "sha512-eXKvpThGzREuAbc6qxnArHh8l8W4AyTcL8IfEnmx+bcnmaSGgjyAHbzZvHZI2csJ+e0MYddl7DX0X7g3sAuXDQ==",
651
             "cpu": [
651
             "cpu": [
652
                 "x64"
652
                 "x64"
653
             ],
653
             ],
659
             ]
659
             ]
660
         },
660
         },
661
         "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
661
         "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
662
-            "version": "4.29.1",
663
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.29.1.tgz",
664
-            "integrity": "sha512-Py5vFd5HWYN9zxBv3WMrLAXY3yYJ6Q/aVERoeUFwiDGiMOWsMs7FokXihSOaT/PMWUty/Pj60XDQndK3eAfE6A==",
662
+            "version": "4.29.2",
663
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.29.2.tgz",
664
+            "integrity": "sha512-h4VgxxmzmtXLLYNDaUcQevCmPYX6zSj4SwKuzY7SR5YlnCBYsmvfYORXgiU8axhkFCDtQF3RW5LIXT8B14Qykg==",
665
             "cpu": [
665
             "cpu": [
666
                 "arm"
666
                 "arm"
667
             ],
667
             ],
673
             ]
673
             ]
674
         },
674
         },
675
         "node_modules/@rollup/rollup-linux-arm-musleabihf": {
675
         "node_modules/@rollup/rollup-linux-arm-musleabihf": {
676
-            "version": "4.29.1",
677
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.29.1.tgz",
678
-            "integrity": "sha512-RiWpGgbayf7LUcuSNIbahr0ys2YnEERD4gYdISA06wa0i8RALrnzflh9Wxii7zQJEB2/Eh74dX4y/sHKLWp5uQ==",
676
+            "version": "4.29.2",
677
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.29.2.tgz",
678
+            "integrity": "sha512-EObwZ45eMmWZQ1w4N7qy4+G1lKHm6mcOwDa+P2+61qxWu1PtQJ/lz2CNJ7W3CkfgN0FQ7cBUy2tk6D5yR4KeXw==",
679
             "cpu": [
679
             "cpu": [
680
                 "arm"
680
                 "arm"
681
             ],
681
             ],
687
             ]
687
             ]
688
         },
688
         },
689
         "node_modules/@rollup/rollup-linux-arm64-gnu": {
689
         "node_modules/@rollup/rollup-linux-arm64-gnu": {
690
-            "version": "4.29.1",
691
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.29.1.tgz",
692
-            "integrity": "sha512-Z80O+taYxTQITWMjm/YqNoe9d10OX6kDh8X5/rFCMuPqsKsSyDilvfg+vd3iXIqtfmp+cnfL1UrYirkaF8SBZA==",
690
+            "version": "4.29.2",
691
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.29.2.tgz",
692
+            "integrity": "sha512-Z7zXVHEXg1elbbYiP/29pPwlJtLeXzjrj4241/kCcECds8Zg9fDfURWbZHRIKrEriAPS8wnVtdl4ZJBvZr325w==",
693
             "cpu": [
693
             "cpu": [
694
                 "arm64"
694
                 "arm64"
695
             ],
695
             ],
701
             ]
701
             ]
702
         },
702
         },
703
         "node_modules/@rollup/rollup-linux-arm64-musl": {
703
         "node_modules/@rollup/rollup-linux-arm64-musl": {
704
-            "version": "4.29.1",
705
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.29.1.tgz",
706
-            "integrity": "sha512-fOHRtF9gahwJk3QVp01a/GqS4hBEZCV1oKglVVq13kcK3NeVlS4BwIFzOHDbmKzt3i0OuHG4zfRP0YoG5OF/rA==",
704
+            "version": "4.29.2",
705
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.29.2.tgz",
706
+            "integrity": "sha512-TF4kxkPq+SudS/r4zGPf0G08Bl7+NZcFrUSR3484WwsHgGgJyPQRLCNrQ/R5J6VzxfEeQR9XRpc8m2t7lD6SEQ==",
707
             "cpu": [
707
             "cpu": [
708
                 "arm64"
708
                 "arm64"
709
             ],
709
             ],
715
             ]
715
             ]
716
         },
716
         },
717
         "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
717
         "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
718
-            "version": "4.29.1",
719
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.29.1.tgz",
720
-            "integrity": "sha512-5a7q3tnlbcg0OodyxcAdrrCxFi0DgXJSoOuidFUzHZ2GixZXQs6Tc3CHmlvqKAmOs5eRde+JJxeIf9DonkmYkw==",
718
+            "version": "4.29.2",
719
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.29.2.tgz",
720
+            "integrity": "sha512-kO9Fv5zZuyj2zB2af4KA29QF6t7YSxKrY7sxZXfw8koDQj9bx5Tk5RjH+kWKFKok0wLGTi4bG117h31N+TIBEg==",
721
             "cpu": [
721
             "cpu": [
722
                 "loong64"
722
                 "loong64"
723
             ],
723
             ],
729
             ]
729
             ]
730
         },
730
         },
731
         "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
731
         "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
732
-            "version": "4.29.1",
733
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.29.1.tgz",
734
-            "integrity": "sha512-9b4Mg5Yfz6mRnlSPIdROcfw1BU22FQxmfjlp/CShWwO3LilKQuMISMTtAu/bxmmrE6A902W2cZJuzx8+gJ8e9w==",
732
+            "version": "4.29.2",
733
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.29.2.tgz",
734
+            "integrity": "sha512-gIh776X7UCBaetVJGdjXPFurGsdWwHHinwRnC5JlLADU8Yk0EdS/Y+dMO264OjJFo7MXQ5PX4xVFbxrwK8zLqA==",
735
             "cpu": [
735
             "cpu": [
736
                 "ppc64"
736
                 "ppc64"
737
             ],
737
             ],
743
             ]
743
             ]
744
         },
744
         },
745
         "node_modules/@rollup/rollup-linux-riscv64-gnu": {
745
         "node_modules/@rollup/rollup-linux-riscv64-gnu": {
746
-            "version": "4.29.1",
747
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.29.1.tgz",
748
-            "integrity": "sha512-G5pn0NChlbRM8OJWpJFMX4/i8OEU538uiSv0P6roZcbpe/WfhEO+AT8SHVKfp8qhDQzaz7Q+1/ixMy7hBRidnQ==",
746
+            "version": "4.29.2",
747
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.29.2.tgz",
748
+            "integrity": "sha512-YgikssQ5UNq1GoFKZydMEkhKbjlUq7G3h8j6yWXLBF24KyoA5BcMtaOUAXq5sydPmOPEqB6kCyJpyifSpCfQ0w==",
749
             "cpu": [
749
             "cpu": [
750
                 "riscv64"
750
                 "riscv64"
751
             ],
751
             ],
757
             ]
757
             ]
758
         },
758
         },
759
         "node_modules/@rollup/rollup-linux-s390x-gnu": {
759
         "node_modules/@rollup/rollup-linux-s390x-gnu": {
760
-            "version": "4.29.1",
761
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.29.1.tgz",
762
-            "integrity": "sha512-WM9lIkNdkhVwiArmLxFXpWndFGuOka4oJOZh8EP3Vb8q5lzdSCBuhjavJsw68Q9AKDGeOOIHYzYm4ZFvmWez5g==",
760
+            "version": "4.29.2",
761
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.29.2.tgz",
762
+            "integrity": "sha512-9ouIR2vFWCyL0Z50dfnon5nOrpDdkTG9lNDs7MRaienQKlTyHcDxplmk3IbhFlutpifBSBr2H4rVILwmMLcaMA==",
763
             "cpu": [
763
             "cpu": [
764
                 "s390x"
764
                 "s390x"
765
             ],
765
             ],
771
             ]
771
             ]
772
         },
772
         },
773
         "node_modules/@rollup/rollup-linux-x64-gnu": {
773
         "node_modules/@rollup/rollup-linux-x64-gnu": {
774
-            "version": "4.29.1",
775
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.29.1.tgz",
776
-            "integrity": "sha512-87xYCwb0cPGZFoGiErT1eDcssByaLX4fc0z2nRM6eMtV9njAfEE6OW3UniAoDhX4Iq5xQVpE6qO9aJbCFumKYQ==",
774
+            "version": "4.29.2",
775
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.29.2.tgz",
776
+            "integrity": "sha512-ckBBNRN/F+NoSUDENDIJ2U9UWmIODgwDB/vEXCPOMcsco1niTkxTXa6D2Y/pvCnpzaidvY2qVxGzLilNs9BSzw==",
777
             "cpu": [
777
             "cpu": [
778
                 "x64"
778
                 "x64"
779
             ],
779
             ],
785
             ]
785
             ]
786
         },
786
         },
787
         "node_modules/@rollup/rollup-linux-x64-musl": {
787
         "node_modules/@rollup/rollup-linux-x64-musl": {
788
-            "version": "4.29.1",
789
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.29.1.tgz",
790
-            "integrity": "sha512-xufkSNppNOdVRCEC4WKvlR1FBDyqCSCpQeMMgv9ZyXqqtKBfkw1yfGMTUTs9Qsl6WQbJnsGboWCp7pJGkeMhKA==",
788
+            "version": "4.29.2",
789
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.29.2.tgz",
790
+            "integrity": "sha512-jycl1wL4AgM2aBFJFlpll/kGvAjhK8GSbEmFT5v3KC3rP/b5xZ1KQmv0vQQ8Bzb2ieFQ0kZFPRMbre/l3Bu9JA==",
791
             "cpu": [
791
             "cpu": [
792
                 "x64"
792
                 "x64"
793
             ],
793
             ],
799
             ]
799
             ]
800
         },
800
         },
801
         "node_modules/@rollup/rollup-win32-arm64-msvc": {
801
         "node_modules/@rollup/rollup-win32-arm64-msvc": {
802
-            "version": "4.29.1",
803
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.29.1.tgz",
804
-            "integrity": "sha512-F2OiJ42m77lSkizZQLuC+jiZ2cgueWQL5YC9tjo3AgaEw+KJmVxHGSyQfDUoYR9cci0lAywv2Clmckzulcq6ig==",
802
+            "version": "4.29.2",
803
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.29.2.tgz",
804
+            "integrity": "sha512-S2V0LlcOiYkNGlRAWZwwUdNgdZBfvsDHW0wYosYFV3c7aKgEVcbonetZXsHv7jRTTX+oY5nDYT4W6B1oUpMNOg==",
805
             "cpu": [
805
             "cpu": [
806
                 "arm64"
806
                 "arm64"
807
             ],
807
             ],
813
             ]
813
             ]
814
         },
814
         },
815
         "node_modules/@rollup/rollup-win32-ia32-msvc": {
815
         "node_modules/@rollup/rollup-win32-ia32-msvc": {
816
-            "version": "4.29.1",
817
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.29.1.tgz",
818
-            "integrity": "sha512-rYRe5S0FcjlOBZQHgbTKNrqxCBUmgDJem/VQTCcTnA2KCabYSWQDrytOzX7avb79cAAweNmMUb/Zw18RNd4mng==",
816
+            "version": "4.29.2",
817
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.29.2.tgz",
818
+            "integrity": "sha512-pW8kioj9H5f/UujdoX2atFlXNQ9aCfAxFRaa+mhczwcsusm6gGrSo4z0SLvqLF5LwFqFTjiLCCzGkNK/LE0utQ==",
819
             "cpu": [
819
             "cpu": [
820
                 "ia32"
820
                 "ia32"
821
             ],
821
             ],
827
             ]
827
             ]
828
         },
828
         },
829
         "node_modules/@rollup/rollup-win32-x64-msvc": {
829
         "node_modules/@rollup/rollup-win32-x64-msvc": {
830
-            "version": "4.29.1",
831
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.29.1.tgz",
832
-            "integrity": "sha512-+10CMg9vt1MoHj6x1pxyjPSMjHTIlqs8/tBztXvPAx24SKs9jwVnKqHJumlH/IzhaPUaj3T6T6wfZr8okdXaIg==",
830
+            "version": "4.29.2",
831
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.29.2.tgz",
832
+            "integrity": "sha512-p6fTArexECPf6KnOHvJXRpAEq0ON1CBtzG/EY4zw08kCHk/kivBc5vUEtnCFNCHOpJZ2ne77fxwRLIKD4wuW2Q==",
833
             "cpu": [
833
             "cpu": [
834
                 "x64"
834
                 "x64"
835
             ],
835
             ],
1300
             }
1300
             }
1301
         },
1301
         },
1302
         "node_modules/fast-glob": {
1302
         "node_modules/fast-glob": {
1303
-            "version": "3.3.2",
1304
-            "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
1305
-            "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
1303
+            "version": "3.3.3",
1304
+            "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
1305
+            "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
1306
             "dev": true,
1306
             "dev": true,
1307
             "license": "MIT",
1307
             "license": "MIT",
1308
             "dependencies": {
1308
             "dependencies": {
1310
                 "@nodelib/fs.walk": "^1.2.3",
1310
                 "@nodelib/fs.walk": "^1.2.3",
1311
                 "glob-parent": "^5.1.2",
1311
                 "glob-parent": "^5.1.2",
1312
                 "merge2": "^1.3.0",
1312
                 "merge2": "^1.3.0",
1313
-                "micromatch": "^4.0.4"
1313
+                "micromatch": "^4.0.8"
1314
             },
1314
             },
1315
             "engines": {
1315
             "engines": {
1316
                 "node": ">=8.6.0"
1316
                 "node": ">=8.6.0"
2242
             }
2242
             }
2243
         },
2243
         },
2244
         "node_modules/rollup": {
2244
         "node_modules/rollup": {
2245
-            "version": "4.29.1",
2246
-            "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.29.1.tgz",
2247
-            "integrity": "sha512-RaJ45M/kmJUzSWDs1Nnd5DdV4eerC98idtUOVr6FfKcgxqvjwHmxc5upLF9qZU9EpsVzzhleFahrT3shLuJzIw==",
2245
+            "version": "4.29.2",
2246
+            "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.29.2.tgz",
2247
+            "integrity": "sha512-tJXpsEkzsEzyAKIaB3qv3IuvTVcTN7qBw1jL4SPPXM3vzDrJgiLGFY6+HodgFaUHAJ2RYJ94zV5MKRJCoQzQeA==",
2248
             "dev": true,
2248
             "dev": true,
2249
             "license": "MIT",
2249
             "license": "MIT",
2250
             "dependencies": {
2250
             "dependencies": {
2258
                 "npm": ">=8.0.0"
2258
                 "npm": ">=8.0.0"
2259
             },
2259
             },
2260
             "optionalDependencies": {
2260
             "optionalDependencies": {
2261
-                "@rollup/rollup-android-arm-eabi": "4.29.1",
2262
-                "@rollup/rollup-android-arm64": "4.29.1",
2263
-                "@rollup/rollup-darwin-arm64": "4.29.1",
2264
-                "@rollup/rollup-darwin-x64": "4.29.1",
2265
-                "@rollup/rollup-freebsd-arm64": "4.29.1",
2266
-                "@rollup/rollup-freebsd-x64": "4.29.1",
2267
-                "@rollup/rollup-linux-arm-gnueabihf": "4.29.1",
2268
-                "@rollup/rollup-linux-arm-musleabihf": "4.29.1",
2269
-                "@rollup/rollup-linux-arm64-gnu": "4.29.1",
2270
-                "@rollup/rollup-linux-arm64-musl": "4.29.1",
2271
-                "@rollup/rollup-linux-loongarch64-gnu": "4.29.1",
2272
-                "@rollup/rollup-linux-powerpc64le-gnu": "4.29.1",
2273
-                "@rollup/rollup-linux-riscv64-gnu": "4.29.1",
2274
-                "@rollup/rollup-linux-s390x-gnu": "4.29.1",
2275
-                "@rollup/rollup-linux-x64-gnu": "4.29.1",
2276
-                "@rollup/rollup-linux-x64-musl": "4.29.1",
2277
-                "@rollup/rollup-win32-arm64-msvc": "4.29.1",
2278
-                "@rollup/rollup-win32-ia32-msvc": "4.29.1",
2279
-                "@rollup/rollup-win32-x64-msvc": "4.29.1",
2261
+                "@rollup/rollup-android-arm-eabi": "4.29.2",
2262
+                "@rollup/rollup-android-arm64": "4.29.2",
2263
+                "@rollup/rollup-darwin-arm64": "4.29.2",
2264
+                "@rollup/rollup-darwin-x64": "4.29.2",
2265
+                "@rollup/rollup-freebsd-arm64": "4.29.2",
2266
+                "@rollup/rollup-freebsd-x64": "4.29.2",
2267
+                "@rollup/rollup-linux-arm-gnueabihf": "4.29.2",
2268
+                "@rollup/rollup-linux-arm-musleabihf": "4.29.2",
2269
+                "@rollup/rollup-linux-arm64-gnu": "4.29.2",
2270
+                "@rollup/rollup-linux-arm64-musl": "4.29.2",
2271
+                "@rollup/rollup-linux-loongarch64-gnu": "4.29.2",
2272
+                "@rollup/rollup-linux-powerpc64le-gnu": "4.29.2",
2273
+                "@rollup/rollup-linux-riscv64-gnu": "4.29.2",
2274
+                "@rollup/rollup-linux-s390x-gnu": "4.29.2",
2275
+                "@rollup/rollup-linux-x64-gnu": "4.29.2",
2276
+                "@rollup/rollup-linux-x64-musl": "4.29.2",
2277
+                "@rollup/rollup-win32-arm64-msvc": "4.29.2",
2278
+                "@rollup/rollup-win32-ia32-msvc": "4.29.2",
2279
+                "@rollup/rollup-win32-x64-msvc": "4.29.2",
2280
                 "fsevents": "~2.3.2"
2280
                 "fsevents": "~2.3.2"
2281
             }
2281
             }
2282
         },
2282
         },

+ 2
- 0
routes/console.php Целия файл

1
 <?php
1
 <?php
2
 
2
 
3
+use App\Console\Commands\TriggerRecurringInvoiceGeneration;
3
 use App\Console\Commands\UpdateOverdueInvoices;
4
 use App\Console\Commands\UpdateOverdueInvoices;
4
 use Illuminate\Support\Facades\Schedule;
5
 use Illuminate\Support\Facades\Schedule;
5
 
6
 
6
 Schedule::command(UpdateOverdueInvoices::class)->everyFiveMinutes();
7
 Schedule::command(UpdateOverdueInvoices::class)->everyFiveMinutes();
8
+Schedule::command(TriggerRecurringInvoiceGeneration::class, ['--queue'])->everyMinute();

Loading…
Отказ
Запис