|  | @@ -13,6 +13,7 @@ use App\Enums\Accounting\DocumentType;
 | 
		
	
		
			
			| 13 | 13 |  use App\Enums\Accounting\EndType;
 | 
		
	
		
			
			| 14 | 14 |  use App\Enums\Accounting\Frequency;
 | 
		
	
		
			
			| 15 | 15 |  use App\Enums\Accounting\IntervalType;
 | 
		
	
		
			
			|  | 16 | +use App\Enums\Accounting\InvoiceStatus;
 | 
		
	
		
			
			| 16 | 17 |  use App\Enums\Accounting\Month;
 | 
		
	
		
			
			| 17 | 18 |  use App\Enums\Accounting\RecurringInvoiceStatus;
 | 
		
	
		
			
			| 18 | 19 |  use App\Enums\Setting\PaymentTerms;
 | 
		
	
	
		
			
			|  | @@ -28,6 +29,7 @@ use Filament\Forms\Form;
 | 
		
	
		
			
			| 28 | 29 |  use Guava\FilamentClusters\Forms\Cluster;
 | 
		
	
		
			
			| 29 | 30 |  use Illuminate\Database\Eloquent\Attributes\CollectedBy;
 | 
		
	
		
			
			| 30 | 31 |  use Illuminate\Database\Eloquent\Attributes\ObservedBy;
 | 
		
	
		
			
			|  | 32 | +use Illuminate\Database\Eloquent\Model;
 | 
		
	
		
			
			| 31 | 33 |  use Illuminate\Database\Eloquent\Relations\BelongsTo;
 | 
		
	
		
			
			| 32 | 34 |  use Illuminate\Database\Eloquent\Relations\HasMany;
 | 
		
	
		
			
			| 33 | 35 |  use Illuminate\Support\Carbon;
 | 
		
	
	
		
			
			|  | @@ -91,6 +93,7 @@ class RecurringInvoice extends Document
 | 
		
	
		
			
			| 91 | 93 |          'payment_terms' => PaymentTerms::class,
 | 
		
	
		
			
			| 92 | 94 |          'frequency' => Frequency::class,
 | 
		
	
		
			
			| 93 | 95 |          'interval_type' => IntervalType::class,
 | 
		
	
		
			
			|  | 96 | +        'interval_value' => 'integer',
 | 
		
	
		
			
			| 94 | 97 |          'month' => Month::class,
 | 
		
	
		
			
			| 95 | 98 |          'day_of_month' => DayOfMonth::class,
 | 
		
	
		
			
			| 96 | 99 |          'day_of_week' => DayOfWeek::class,
 | 
		
	
	
		
			
			|  | @@ -223,9 +226,6 @@ class RecurringInvoice extends Document
 | 
		
	
		
			
			| 223 | 226 |          return "Repeat every {$interval}{$dayDescription}";
 | 
		
	
		
			
			| 224 | 227 |      }
 | 
		
	
		
			
			| 225 | 228 |  
 | 
		
	
		
			
			| 226 |  | -    /**
 | 
		
	
		
			
			| 227 |  | -     * Get a human-readable description of when the schedule ends.
 | 
		
	
		
			
			| 228 |  | -     */
 | 
		
	
		
			
			| 229 | 229 |      public function getEndDescription(): string
 | 
		
	
		
			
			| 230 | 230 |      {
 | 
		
	
		
			
			| 231 | 231 |          if (! $this->end_type) {
 | 
		
	
	
		
			
			|  | @@ -243,9 +243,6 @@ class RecurringInvoice extends Document
 | 
		
	
		
			
			| 243 | 243 |          };
 | 
		
	
		
			
			| 244 | 244 |      }
 | 
		
	
		
			
			| 245 | 245 |  
 | 
		
	
		
			
			| 246 |  | -    /**
 | 
		
	
		
			
			| 247 |  | -     * Get the schedule timeline description.
 | 
		
	
		
			
			| 248 |  | -     */
 | 
		
	
		
			
			| 249 | 246 |      public function getTimelineDescription(): string
 | 
		
	
		
			
			| 250 | 247 |      {
 | 
		
	
		
			
			| 251 | 248 |          $parts = [];
 | 
		
	
	
		
			
			|  | @@ -261,12 +258,14 @@ class RecurringInvoice extends Document
 | 
		
	
		
			
			| 261 | 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 | 269 |          if (! $lastDate) {
 | 
		
	
		
			
			| 271 | 270 |              return null;
 | 
		
	
		
			
			| 272 | 271 |          }
 | 
		
	
	
		
			
			|  | @@ -274,59 +273,109 @@ class RecurringInvoice extends Document
 | 
		
	
		
			
			| 274 | 273 |          $nextDate = match (true) {
 | 
		
	
		
			
			| 275 | 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 | 282 |              $this->frequency->isCustom() => $this->calculateCustomNextDate($lastDate),
 | 
		
	
		
			
			| 284 | 283 |  
 | 
		
	
		
			
			| 285 | 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 | 288 |              return null;
 | 
		
	
		
			
			| 291 | 289 |          }
 | 
		
	
		
			
			| 292 | 290 |  
 | 
		
	
		
			
			| 293 | 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 | 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 | 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 | 379 |      public function hasReachedEnd(?Carbon $nextDate = null): bool
 | 
		
	
		
			
			| 331 | 380 |      {
 | 
		
	
		
			
			| 332 | 381 |          if (! $this->end_type) {
 | 
		
	
	
		
			
			|  | @@ -464,18 +513,74 @@ class RecurringInvoice extends Document
 | 
		
	
		
			
			| 464 | 513 |                              ->visible(
 | 
		
	
		
			
			| 465 | 514 |                                  fn (Forms\Get $get) => Frequency::parse($get('frequency'))->isYearly() ||
 | 
		
	
		
			
			| 466 | 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 | 539 |                          Forms\Components\Select::make('day_of_month')
 | 
		
	
		
			
			| 470 | 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 | 550 |                              ->softRequired()
 | 
		
	
		
			
			| 473 | 551 |                              ->visible(
 | 
		
	
		
			
			| 474 | 552 |                                  fn (Forms\Get $get) => Frequency::parse($get('frequency'))?->isMonthly() ||
 | 
		
	
		
			
			| 475 | 553 |                                  Frequency::parse($get('frequency'))?->isYearly() ||
 | 
		
	
		
			
			| 476 | 554 |                                  IntervalType::parse($get('interval_type'))?->isMonth() ||
 | 
		
	
		
			
			| 477 | 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 | 585 |                          Forms\Components\Select::make('day_of_week')
 | 
		
	
		
			
			| 481 | 586 |                              ->label('Day of Week')
 | 
		
	
	
		
			
			|  | @@ -484,7 +589,17 @@ class RecurringInvoice extends Document
 | 
		
	
		
			
			| 484 | 589 |                              ->visible(
 | 
		
	
		
			
			| 485 | 590 |                                  fn (Forms\Get $get) => Frequency::parse($get('frequency'))?->isWeekly() ||
 | 
		
	
		
			
			| 486 | 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 | 603 |                      ])->columns(2),
 | 
		
	
		
			
			| 489 | 604 |  
 | 
		
	
		
			
			| 490 | 605 |                  CustomSection::make('Dates & Time')
 | 
		
	
	
		
			
			|  | @@ -492,7 +607,17 @@ class RecurringInvoice extends Document
 | 
		
	
		
			
			| 492 | 607 |                      ->schema([
 | 
		
	
		
			
			| 493 | 608 |                          Forms\Components\DatePicker::make('start_date')
 | 
		
	
		
			
			| 494 | 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 | 622 |                          Forms\Components\Group::make(function (Forms\Get $get) {
 | 
		
	
		
			
			| 498 | 623 |                              $components = [];
 | 
		
	
	
		
			
			|  | @@ -587,4 +712,105 @@ class RecurringInvoice extends Document
 | 
		
	
		
			
			| 587 | 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 |  }
 |