Andrew Wallo 8 months ago
parent
commit
267aaba680
34 changed files with 588 additions and 318 deletions
  1. 1
    1
      app/Concerns/ManagesLineItems.php
  2. 24
    2
      app/DTO/ClientDTO.php
  3. 32
    7
      app/DTO/CompanyDTO.php
  4. 5
    0
      app/Enums/Accounting/DayOfMonth.php
  5. 2
    4
      app/Filament/Company/Clusters/Settings/Pages/CompanyProfile.php
  6. 1
    1
      app/Filament/Company/Clusters/Settings/Pages/Localization.php
  7. 2
    2
      app/Filament/Company/Resources/Sales/ClientResource/Pages/CreateClient.php
  8. 2
    2
      app/Filament/Company/Resources/Sales/ClientResource/Pages/EditClient.php
  9. 4
    1
      app/Filament/Company/Resources/Sales/InvoiceResource.php
  10. 10
    3
      app/Filament/Company/Resources/Sales/InvoiceResource/Pages/CreateInvoice.php
  11. 3
    5
      app/Filament/Company/Resources/Sales/RecurringInvoiceResource/Pages/ViewRecurringInvoice.php
  12. 1
    1
      app/Filament/Forms/Components/AddressFields.php
  13. 47
    0
      app/Filament/Forms/Components/Banner.php
  14. 4
    1
      app/Filament/Forms/Components/StateSelect.php
  15. 47
    0
      app/Filament/Infolists/Components/BannerEntry.php
  16. 22
    17
      app/Models/Accounting/RecurringInvoice.php
  17. 3
    3
      app/Models/Common/Address.php
  18. 7
    5
      app/Models/Locale/Country.php
  19. 35
    5
      app/Models/Locale/State.php
  20. 13
    2
      app/Providers/MacroServiceProvider.php
  21. 8
    2
      app/Utilities/Currency/CurrencyConverter.php
  22. 23
    23
      composer.lock
  23. 5
    5
      database/factories/Accounting/RecurringInvoiceFactory.php
  24. 1
    1
      database/factories/Common/AddressFactory.php
  25. 1
    1
      database/factories/CompanyFactory.php
  26. 1
    1
      database/migrations/2024_11_19_225812_create_addresses_table.php
  27. 79
    79
      package-lock.json
  28. 4
    17
      resources/views/filament/infolists/components/document-preview.blade.php
  29. 0
    0
      resources/views/vendor/filament-simple-alert/.gitkeep
  30. 0
    15
      resources/views/vendor/filament-simple-alert/components/simple-alert-entry.blade.php
  31. 0
    15
      resources/views/vendor/filament-simple-alert/components/simple-alert-field.blade.php
  32. 0
    84
      resources/views/vendor/filament-simple-alert/components/simple-alert.blade.php
  33. 187
    13
      tests/Feature/Accounting/RecurringInvoiceTest.php
  34. 14
    0
      tests/TestCase.php

+ 1
- 1
app/Concerns/ManagesLineItems.php View File

81
 
81
 
82
     protected function updateDocumentTotals(Model $record, array $data): array
82
     protected function updateDocumentTotals(Model $record, array $data): array
83
     {
83
     {
84
-        $currencyCode = $data['currency_code'] ?? CurrencyAccessor::getDefaultCurrency();
84
+        $currencyCode = $data['currency_code'] ?? $record->currency_code ?? CurrencyAccessor::getDefaultCurrency();
85
         $subtotalCents = $record->lineItems()->sum('subtotal');
85
         $subtotalCents = $record->lineItems()->sum('subtotal');
86
         $taxTotalCents = $record->lineItems()->sum('tax_total');
86
         $taxTotalCents = $record->lineItems()->sum('tax_total');
87
         $discountTotalCents = $this->calculateDiscountTotal(
87
         $discountTotalCents = $this->calculateDiscountTotal(

+ 24
- 2
app/DTO/ClientDTO.php View File

25
             addressLine1: $address?->address_line_1 ?? '',
25
             addressLine1: $address?->address_line_1 ?? '',
26
             addressLine2: $address?->address_line_2 ?? '',
26
             addressLine2: $address?->address_line_2 ?? '',
27
             city: $address?->city ?? '',
27
             city: $address?->city ?? '',
28
-            state: $address?->state ?? '',
28
+            state: $address?->state?->name ?? '',
29
             postalCode: $address?->postal_code ?? '',
29
             postalCode: $address?->postal_code ?? '',
30
-            country: $address?->country ?? '',
30
+            country: $address?->country?->name ?? '',
31
         );
31
         );
32
     }
32
     }
33
+
34
+    public function getFormattedAddressHtml(): ?string
35
+    {
36
+        if (empty($this->addressLine1)) {
37
+            return null;
38
+        }
39
+
40
+        $lines = array_filter([
41
+            $this->addressLine1,
42
+            $this->addressLine2,
43
+            implode(', ', array_filter([
44
+                $this->city,
45
+                $this->state,
46
+                $this->postalCode,
47
+            ])),
48
+            $this->country,
49
+        ]);
50
+
51
+        return collect($lines)
52
+            ->map(static fn ($line) => "<p>{$line}</p>")
53
+            ->join('');
54
+    }
33
 }
55
 }

+ 32
- 7
app/DTO/CompanyDTO.php View File

8
 {
8
 {
9
     public function __construct(
9
     public function __construct(
10
         public string $name,
10
         public string $name,
11
-        public string $address,
11
+        public string $addressLine1,
12
+        public string $addressLine2,
12
         public string $city,
13
         public string $city,
13
         public string $state,
14
         public string $state,
14
-        public string $zipCode,
15
+        public string $postalCode,
15
         public string $country,
16
         public string $country,
16
     ) {}
17
     ) {}
17
 
18
 
18
     public static function fromModel(Company $company): self
19
     public static function fromModel(Company $company): self
19
     {
20
     {
20
         $profile = $company->profile;
21
         $profile = $company->profile;
22
+        $address = $profile->address ?? null;
21
 
23
 
22
         return new self(
24
         return new self(
23
             name: $company->name,
25
             name: $company->name,
24
-            address: $profile->address ?? '',
25
-            city: $profile->city?->name ?? '',
26
-            state: $profile->state?->name ?? '',
27
-            zipCode: $profile->zip_code ?? '',
28
-            country: $profile->state?->country->name ?? '',
26
+            addressLine1: $address?->address_line_1 ?? '',
27
+            addressLine2: $address?->address_line_2 ?? '',
28
+            city: $address?->city ?? '',
29
+            state: $address?->state?->name ?? '',
30
+            postalCode: $address?->postal_code ?? '',
31
+            country: $address?->country?->name ?? '',
29
         );
32
         );
30
     }
33
     }
34
+
35
+    public function getFormattedAddressHtml(): ?string
36
+    {
37
+        if (empty($this->addressLine1)) {
38
+            return null;
39
+        }
40
+
41
+        $lines = array_filter([
42
+            $this->addressLine1,
43
+            $this->addressLine2,
44
+            implode(', ', array_filter([
45
+                $this->city,
46
+                $this->state,
47
+                $this->postalCode,
48
+            ])),
49
+            $this->country,
50
+        ]);
51
+
52
+        return collect($lines)
53
+            ->map(static fn ($line) => "<p>{$line}</p>")
54
+            ->join('');
55
+    }
31
 }
56
 }

+ 5
- 0
app/Enums/Accounting/DayOfMonth.php View File

100
 
100
 
101
         return $date->day(min($this->value, $date->daysInMonth));
101
         return $date->day(min($this->value, $date->daysInMonth));
102
     }
102
     }
103
+
104
+    public function mayExceedMonthLength(): bool
105
+    {
106
+        return $this->value > 28;
107
+    }
103
 }
108
 }

+ 2
- 4
app/Filament/Company/Clusters/Settings/Pages/CompanyProfile.php View File

5
 use App\Enums\Setting\EntityType;
5
 use App\Enums\Setting\EntityType;
6
 use App\Filament\Company\Clusters\Settings;
6
 use App\Filament\Company\Clusters\Settings;
7
 use App\Filament\Forms\Components\AddressFields;
7
 use App\Filament\Forms\Components\AddressFields;
8
+use App\Filament\Forms\Components\Banner;
8
 use App\Models\Setting\CompanyProfile as CompanyProfileModel;
9
 use App\Models\Setting\CompanyProfile as CompanyProfileModel;
9
 use App\Utilities\Localization\Timezone;
10
 use App\Utilities\Localization\Timezone;
10
-use CodeWithDennis\SimpleAlert\Components\Forms\SimpleAlert;
11
 use Filament\Actions\Action;
11
 use Filament\Actions\Action;
12
 use Filament\Actions\ActionGroup;
12
 use Filament\Actions\ActionGroup;
13
 use Filament\Forms\Components\Component;
13
 use Filament\Forms\Components\Component;
185
 
185
 
186
     protected function getNeedsAddressCompletionAlert(): Component
186
     protected function getNeedsAddressCompletionAlert(): Component
187
     {
187
     {
188
-        return SimpleAlert::make('needsAddressCompletion')
188
+        return Banner::make('needsAddressCompletion')
189
             ->warning()
189
             ->warning()
190
-            ->border()
191
-            ->icon('heroicon-o-exclamation-triangle')
192
             ->title('Address Information Incomplete')
190
             ->title('Address Information Incomplete')
193
             ->description('Please complete the required address information for proper business operations.')
191
             ->description('Please complete the required address information for proper business operations.')
194
             ->visible(fn (CompanyProfileModel $record) => $record->address->isIncomplete())
192
             ->visible(fn (CompanyProfileModel $record) => $record->address->isIncomplete())

+ 1
- 1
app/Filament/Company/Clusters/Settings/Pages/Localization.php View File

130
                 Select::make('timezone')
130
                 Select::make('timezone')
131
                     ->softRequired()
131
                     ->softRequired()
132
                     ->localizeLabel()
132
                     ->localizeLabel()
133
-                    ->options(Timezone::getTimezoneOptions(CompanyProfileModel::first()->address->country))
133
+                    ->options(Timezone::getTimezoneOptions(CompanyProfileModel::first()->address->country_code))
134
                     ->searchable(),
134
                     ->searchable(),
135
             ])->columns();
135
             ])->columns();
136
     }
136
     }

+ 2
- 2
app/Filament/Company/Resources/Sales/ClientResource/Pages/CreateClient.php View File

48
                 'parent_address_id' => $billingAddress->id,
48
                 'parent_address_id' => $billingAddress->id,
49
                 'address_line_1' => $billingAddress->address_line_1,
49
                 'address_line_1' => $billingAddress->address_line_1,
50
                 'address_line_2' => $billingAddress->address_line_2,
50
                 'address_line_2' => $billingAddress->address_line_2,
51
-                'country' => $billingAddress->country,
51
+                'country_code' => $billingAddress->country_code,
52
                 'state_id' => $billingAddress->state_id,
52
                 'state_id' => $billingAddress->state_id,
53
                 'city' => $billingAddress->city,
53
                 'city' => $billingAddress->city,
54
                 'postal_code' => $billingAddress->postal_code,
54
                 'postal_code' => $billingAddress->postal_code,
58
                 ...$shippingAddress,
58
                 ...$shippingAddress,
59
                 'address_line_1' => $shippingData['address_line_1'],
59
                 'address_line_1' => $shippingData['address_line_1'],
60
                 'address_line_2' => $shippingData['address_line_2'],
60
                 'address_line_2' => $shippingData['address_line_2'],
61
-                'country' => $shippingData['country'],
61
+                'country_code' => $shippingData['country_code'],
62
                 'state_id' => $shippingData['state_id'],
62
                 'state_id' => $shippingData['state_id'],
63
                 'city' => $shippingData['city'],
63
                 'city' => $shippingData['city'],
64
                 'postal_code' => $shippingData['postal_code'],
64
                 'postal_code' => $shippingData['postal_code'],

+ 2
- 2
app/Filament/Company/Resources/Sales/ClientResource/Pages/EditClient.php View File

53
                 'parent_address_id' => $billingAddress->id,
53
                 'parent_address_id' => $billingAddress->id,
54
                 'address_line_1' => $billingAddress->address_line_1,
54
                 'address_line_1' => $billingAddress->address_line_1,
55
                 'address_line_2' => $billingAddress->address_line_2,
55
                 'address_line_2' => $billingAddress->address_line_2,
56
-                'country' => $billingAddress->country,
56
+                'country_code' => $billingAddress->country_code,
57
                 'state_id' => $billingAddress->state_id,
57
                 'state_id' => $billingAddress->state_id,
58
                 'city' => $billingAddress->city,
58
                 'city' => $billingAddress->city,
59
                 'postal_code' => $billingAddress->postal_code,
59
                 'postal_code' => $billingAddress->postal_code,
64
                 'parent_address_id' => null,
64
                 'parent_address_id' => null,
65
                 'address_line_1' => $shippingData['address_line_1'],
65
                 'address_line_1' => $shippingData['address_line_1'],
66
                 'address_line_2' => $shippingData['address_line_2'],
66
                 'address_line_2' => $shippingData['address_line_2'],
67
-                'country' => $shippingData['country'],
67
+                'country_code' => $shippingData['country_code'],
68
                 'state_id' => $shippingData['state_id'],
68
                 'state_id' => $shippingData['state_id'],
69
                 'city' => $shippingData['city'],
69
                 'city' => $shippingData['city'],
70
                 'postal_code' => $shippingData['postal_code'],
70
                 'postal_code' => $shippingData['postal_code'],

+ 4
- 1
app/Filament/Company/Resources/Sales/InvoiceResource.php View File

115
                                             $set('currency_code', $currencyCode);
115
                                             $set('currency_code', $currencyCode);
116
                                         }
116
                                         }
117
                                     }),
117
                                     }),
118
-                                CreateCurrencySelect::make('currency_code'),
118
+                                CreateCurrencySelect::make('currency_code')
119
+                                    ->disabled(function (?Invoice $record) {
120
+                                        return $record?->hasPayments();
121
+                                    }),
119
                             ]),
122
                             ]),
120
                             Forms\Components\Group::make([
123
                             Forms\Components\Group::make([
121
                                 Forms\Components\TextInput::make('invoice_number')
124
                                 Forms\Components\TextInput::make('invoice_number')

+ 10
- 3
app/Filament/Company/Resources/Sales/InvoiceResource/Pages/CreateInvoice.php View File

6
 use App\Concerns\RedirectToListPage;
6
 use App\Concerns\RedirectToListPage;
7
 use App\Filament\Company\Resources\Sales\InvoiceResource;
7
 use App\Filament\Company\Resources\Sales\InvoiceResource;
8
 use App\Models\Accounting\Invoice;
8
 use App\Models\Accounting\Invoice;
9
+use App\Models\Common\Client;
9
 use Filament\Resources\Pages\CreateRecord;
10
 use Filament\Resources\Pages\CreateRecord;
10
 use Filament\Support\Enums\MaxWidth;
11
 use Filament\Support\Enums\MaxWidth;
11
 use Illuminate\Database\Eloquent\Model;
12
 use Illuminate\Database\Eloquent\Model;
13
+use Livewire\Attributes\Url;
12
 
14
 
13
 class CreateInvoice extends CreateRecord
15
 class CreateInvoice extends CreateRecord
14
 {
16
 {
17
 
19
 
18
     protected static string $resource = InvoiceResource::class;
20
     protected static string $resource = InvoiceResource::class;
19
 
21
 
22
+    #[Url(as: 'client')]
23
+    public ?int $clientId = null;
24
+
20
     public function mount(): void
25
     public function mount(): void
21
     {
26
     {
22
         parent::mount();
27
         parent::mount();
23
 
28
 
24
-        $clientId = request()->query('client');
29
+        if ($this->clientId) {
30
+            $this->data['client_id'] = $this->clientId;
25
 
31
 
26
-        if ($clientId) {
27
-            $this->data['client_id'] = $clientId;
32
+            if ($currencyCode = Client::find($this->clientId)?->currency_code) {
33
+                $this->data['currency_code'] = $currencyCode;
34
+            }
28
         }
35
         }
29
     }
36
     }
30
 
37
 

+ 3
- 5
app/Filament/Company/Resources/Sales/RecurringInvoiceResource/Pages/ViewRecurringInvoice.php View File

6
 use App\Filament\Company\Resources\Sales\ClientResource;
6
 use App\Filament\Company\Resources\Sales\ClientResource;
7
 use App\Filament\Company\Resources\Sales\InvoiceResource\Pages\ListInvoices;
7
 use App\Filament\Company\Resources\Sales\InvoiceResource\Pages\ListInvoices;
8
 use App\Filament\Company\Resources\Sales\RecurringInvoiceResource;
8
 use App\Filament\Company\Resources\Sales\RecurringInvoiceResource;
9
+use App\Filament\Infolists\Components\BannerEntry;
9
 use App\Filament\Infolists\Components\DocumentPreview;
10
 use App\Filament\Infolists\Components\DocumentPreview;
10
 use App\Models\Accounting\RecurringInvoice;
11
 use App\Models\Accounting\RecurringInvoice;
11
-use CodeWithDennis\SimpleAlert\Components\Infolists\SimpleAlert;
12
 use Filament\Actions;
12
 use Filament\Actions;
13
 use Filament\Infolists\Components\Actions\Action;
13
 use Filament\Infolists\Components\Actions\Action;
14
 use Filament\Infolists\Components\Grid;
14
 use Filament\Infolists\Components\Grid;
54
     {
54
     {
55
         return $infolist
55
         return $infolist
56
             ->schema([
56
             ->schema([
57
-                SimpleAlert::make('scheduleIsNotSet')
57
+                BannerEntry::make('scheduleIsNotSet')
58
                     ->info()
58
                     ->info()
59
-                    ->border()
60
-                    ->icon('heroicon-o-information-circle')
61
                     ->title('Schedule Not Set')
59
                     ->title('Schedule Not Set')
62
                     ->description('The schedule for this recurring invoice has not been set. You must set a schedule before you can approve this draft and start creating invoices.')
60
                     ->description('The schedule for this recurring invoice has not been set. You must set a schedule before you can approve this draft and start creating invoices.')
63
                     ->visible(fn (RecurringInvoice $record) => ! $record->hasValidStartDate())
61
                     ->visible(fn (RecurringInvoice $record) => ! $record->hasValidStartDate())
66
                         RecurringInvoice::getUpdateScheduleAction(Action::class)
64
                         RecurringInvoice::getUpdateScheduleAction(Action::class)
67
                             ->outlined(),
65
                             ->outlined(),
68
                     ]),
66
                     ]),
69
-                SimpleAlert::make('readyToApprove')
67
+                BannerEntry::make('readyToApprove')
70
                     ->info()
68
                     ->info()
71
                     ->title('Ready to Approve')
69
                     ->title('Ready to Approve')
72
                     ->description('This recurring invoice is ready for approval. Review the details, and approve it when you’re ready to start generating invoices.')
70
                     ->description('This recurring invoice is ready for approval. Review the details, and approve it when you’re ready to start generating invoices.')

+ 1
- 1
app/Filament/Forms/Components/AddressFields.php View File

22
             TextInput::make('address_line_2')
22
             TextInput::make('address_line_2')
23
                 ->label('Address Line 2')
23
                 ->label('Address Line 2')
24
                 ->maxLength(255),
24
                 ->maxLength(255),
25
-            CountrySelect::make('country')
25
+            CountrySelect::make('country_code')
26
                 ->clearStateField()
26
                 ->clearStateField()
27
                 ->required(),
27
                 ->required(),
28
             StateSelect::make('state_id'),
28
             StateSelect::make('state_id'),

+ 47
- 0
app/Filament/Forms/Components/Banner.php View File

1
+<?php
2
+
3
+namespace App\Filament\Forms\Components;
4
+
5
+use CodeWithDennis\SimpleAlert\Components\Forms\SimpleAlert;
6
+
7
+class Banner extends SimpleAlert
8
+{
9
+    protected function setUp(): void
10
+    {
11
+        parent::setUp();
12
+
13
+        $this->border();
14
+    }
15
+
16
+    public function danger(): static
17
+    {
18
+        $this->color = 'danger';
19
+        $this->icon = 'heroicon-o-x-circle';
20
+
21
+        return $this;
22
+    }
23
+
24
+    public function info(): static
25
+    {
26
+        $this->color = 'info';
27
+        $this->icon = 'heroicon-o-information-circle';
28
+
29
+        return $this;
30
+    }
31
+
32
+    public function success(): static
33
+    {
34
+        $this->color = 'success';
35
+        $this->icon = 'heroicon-o-check-circle';
36
+
37
+        return $this;
38
+    }
39
+
40
+    public function warning(): static
41
+    {
42
+        $this->color = 'warning';
43
+        $this->icon = 'heroicon-o-exclamation-triangle';
44
+
45
+        return $this;
46
+    }
47
+}

+ 4
- 1
app/Filament/Forms/Components/StateSelect.php View File

15
         $this
15
         $this
16
             ->localizeLabel('State / Province')
16
             ->localizeLabel('State / Province')
17
             ->searchable()
17
             ->searchable()
18
-            ->options(static fn (Get $get) => State::getStateOptions($get('country')));
18
+            ->options(static fn (Get $get) => State::getStateOptions($get('country_code')))
19
+            ->getSearchResultsUsing(static function (string $search, Get $get): array {
20
+                return State::getSearchResultsUsing($search, $get('country_code'));
21
+            });
19
     }
22
     }
20
 }
23
 }

+ 47
- 0
app/Filament/Infolists/Components/BannerEntry.php View File

1
+<?php
2
+
3
+namespace App\Filament\Infolists\Components;
4
+
5
+use CodeWithDennis\SimpleAlert\Components\Infolists\SimpleAlert;
6
+
7
+class BannerEntry extends SimpleAlert
8
+{
9
+    protected function setUp(): void
10
+    {
11
+        parent::setUp();
12
+
13
+        $this->border();
14
+    }
15
+
16
+    public function danger(): static
17
+    {
18
+        $this->color = 'danger';
19
+        $this->icon = 'heroicon-o-x-circle';
20
+
21
+        return $this;
22
+    }
23
+
24
+    public function info(): static
25
+    {
26
+        $this->color = 'info';
27
+        $this->icon = 'heroicon-o-information-circle';
28
+
29
+        return $this;
30
+    }
31
+
32
+    public function success(): static
33
+    {
34
+        $this->color = 'success';
35
+        $this->icon = 'heroicon-o-check-circle';
36
+
37
+        return $this;
38
+    }
39
+
40
+    public function warning(): static
41
+    {
42
+        $this->color = 'warning';
43
+        $this->icon = 'heroicon-o-exclamation-triangle';
44
+
45
+        return $this;
46
+    }
47
+}

+ 22
- 17
app/Models/Accounting/RecurringInvoice.php View File

17
 use App\Enums\Accounting\Month;
17
 use App\Enums\Accounting\Month;
18
 use App\Enums\Accounting\RecurringInvoiceStatus;
18
 use App\Enums\Accounting\RecurringInvoiceStatus;
19
 use App\Enums\Setting\PaymentTerms;
19
 use App\Enums\Setting\PaymentTerms;
20
+use App\Filament\Forms\Components\Banner;
20
 use App\Filament\Forms\Components\CustomSection;
21
 use App\Filament\Forms\Components\CustomSection;
21
 use App\Models\Common\Client;
22
 use App\Models\Common\Client;
22
 use App\Models\Setting\CompanyProfile;
23
 use App\Models\Setting\CompanyProfile;
23
 use App\Observers\RecurringInvoiceObserver;
24
 use App\Observers\RecurringInvoiceObserver;
24
 use App\Support\ScheduleHandler;
25
 use App\Support\ScheduleHandler;
25
 use App\Utilities\Localization\Timezone;
26
 use App\Utilities\Localization\Timezone;
26
-use CodeWithDennis\SimpleAlert\Components\Forms\SimpleAlert;
27
 use Filament\Actions\Action;
27
 use Filament\Actions\Action;
28
 use Filament\Actions\MountableAction;
28
 use Filament\Actions\MountableAction;
29
 use Filament\Forms;
29
 use Filament\Forms;
311
         return $nextDate;
311
         return $nextDate;
312
     }
312
     }
313
 
313
 
314
-    public function calculateNextWeeklyDate(Carbon $lastDate): ?Carbon
314
+    public function calculateNextWeeklyDate(Carbon $lastDate, int $interval = 1): ?Carbon
315
     {
315
     {
316
-        return $lastDate->copy()->next($this->day_of_week->name);
316
+        return $lastDate->copy()
317
+            ->addWeeks($interval - 1)
318
+            ->next($this->day_of_week->value);
317
     }
319
     }
318
 
320
 
319
-    public function calculateNextMonthlyDate(Carbon $lastDate): ?Carbon
321
+    public function calculateNextMonthlyDate(Carbon $lastDate, int $interval = 1): ?Carbon
320
     {
322
     {
321
-        return $this->day_of_month->resolveDate($lastDate->copy()->addMonth());
323
+        return $this->day_of_month->resolveDate(
324
+            $lastDate->copy()->addMonthsNoOverflow($interval)
325
+        );
322
     }
326
     }
323
 
327
 
324
-    public function calculateNextYearlyDate(Carbon $lastDate): ?Carbon
328
+    public function calculateNextYearlyDate(Carbon $lastDate, int $interval = 1): ?Carbon
325
     {
329
     {
326
-        return $this->day_of_month->resolveDate($lastDate->copy()->addYear()->month($this->month->value));
330
+        return $this->day_of_month->resolveDate(
331
+            $lastDate->copy()->addYears($interval)->month($this->month->value)
332
+        );
327
     }
333
     }
328
 
334
 
329
     protected function calculateCustomNextDate(Carbon $lastDate): ?Carbon
335
     protected function calculateCustomNextDate(Carbon $lastDate): ?Carbon
333
         return match ($this->interval_type) {
339
         return match ($this->interval_type) {
334
             IntervalType::Day => $lastDate->copy()->addDays($interval),
340
             IntervalType::Day => $lastDate->copy()->addDays($interval),
335
 
341
 
336
-            IntervalType::Week => $lastDate->copy()->addWeeks($interval),
342
+            IntervalType::Week => $this->calculateNextWeeklyDate($lastDate, $interval),
337
 
343
 
338
-            IntervalType::Month => $this->day_of_month->resolveDate($lastDate->copy()->addMonths($interval)),
344
+            IntervalType::Month => $this->calculateNextMonthlyDate($lastDate, $interval),
339
 
345
 
340
-            IntervalType::Year => $this->day_of_month->resolveDate($lastDate->copy()->addYears($interval)->month($this->month->value)),
346
+            IntervalType::Year => $this->calculateNextYearlyDate($lastDate, $interval),
341
 
347
 
342
             default => null
348
             default => null
343
         };
349
         };
408
                                     $handler->handleFrequencyChange($state);
414
                                     $handler->handleFrequencyChange($state);
409
                                 }),
415
                                 }),
410
 
416
 
411
-                            // Custom frequency fields in a nested grid
412
                             Cluster::make([
417
                             Cluster::make([
413
                                 Forms\Components\TextInput::make('interval_value')
418
                                 Forms\Components\TextInput::make('interval_value')
414
                                     ->softRequired()
419
                                     ->softRequired()
430
                                 ->markAsRequired(false)
435
                                 ->markAsRequired(false)
431
                                 ->visible($frequency->isCustom()),
436
                                 ->visible($frequency->isCustom()),
432
 
437
 
433
-                            // Specific schedule details
434
                             Forms\Components\Select::make('month')
438
                             Forms\Components\Select::make('month')
435
                                 ->label('Month')
439
                                 ->label('Month')
436
                                 ->options(Month::class)
440
                                 ->options(Month::class)
463
                                     $handler->handleDateChange('day_of_month', $state);
467
                                     $handler->handleDateChange('day_of_month', $state);
464
                                 }),
468
                                 }),
465
 
469
 
466
-                            SimpleAlert::make('dayOfMonthNotice')
467
-                                ->title(function () use ($dayOfMonth) {
468
-                                    return "The invoice will be created on the {$dayOfMonth->getLabel()} day of each month, or on the last day for months ending earlier.";
470
+                            Banner::make('dayOfMonthNotice')
471
+                                ->info()
472
+                                ->title(static function () use ($dayOfMonth) {
473
+                                    return "For months with fewer than {$dayOfMonth->value} days, the last day of the month will be used.";
469
                                 })
474
                                 })
470
                                 ->columnSpanFull()
475
                                 ->columnSpanFull()
471
-                                ->visible($dayOfMonth?->value > 28),
476
+                                ->visible($dayOfMonth?->mayExceedMonthLength() && ($frequency->isMonthly() || $intervalType?->isMonth())),
472
 
477
 
473
                             Forms\Components\Select::make('day_of_week')
478
                             Forms\Components\Select::make('day_of_week')
474
                                 ->label('Day of Week')
479
                                 ->label('Day of Week')
475
                                 ->options(DayOfWeek::class)
480
                                 ->options(DayOfWeek::class)
476
                                 ->softRequired()
481
                                 ->softRequired()
477
-                                ->visible($frequency->isWeekly() || $intervalType?->isWeek())
482
+                                ->visible(($frequency->isWeekly() || $intervalType?->isWeek()) ?? false)
478
                                 ->live()
483
                                 ->live()
479
                                 ->afterStateUpdated(function (Forms\Set $set, $state) {
484
                                 ->afterStateUpdated(function (Forms\Set $set, $state) {
480
                                     $handler = new ScheduleHandler($set);
485
                                     $handler = new ScheduleHandler($set);

+ 3
- 3
app/Models/Common/Address.php View File

33
         'city',
33
         'city',
34
         'state_id',
34
         'state_id',
35
         'postal_code',
35
         'postal_code',
36
-        'country',
36
+        'country_code',
37
         'notes',
37
         'notes',
38
         'created_by',
38
         'created_by',
39
         'updated_by',
39
         'updated_by',
60
 
60
 
61
     public function country(): BelongsTo
61
     public function country(): BelongsTo
62
     {
62
     {
63
-        return $this->belongsTo(Country::class, 'country', 'id');
63
+        return $this->belongsTo(Country::class, 'country_code', 'id');
64
     }
64
     }
65
 
65
 
66
     public function state(): BelongsTo
66
     public function state(): BelongsTo
80
                 implode(', ', $street), // Street 1 & 2 on same line if both exist
80
                 implode(', ', $street), // Street 1 & 2 on same line if both exist
81
                 implode(', ', array_filter([
81
                 implode(', ', array_filter([
82
                     $this->city,
82
                     $this->city,
83
-                    $this->state->state_code,
83
+                    $this->state->name,
84
                     $this->postal_code,
84
                     $this->postal_code,
85
                 ])),
85
                 ])),
86
             ]);
86
             ]);

+ 7
- 5
app/Models/Locale/Country.php View File

51
 
51
 
52
     public function addresses(): HasMany
52
     public function addresses(): HasMany
53
     {
53
     {
54
-        return $this->hasMany(Address::class, 'country', 'id');
54
+        return $this->hasMany(Address::class, 'country_code', 'id');
55
     }
55
     }
56
 
56
 
57
     public function states(): HasMany
57
     public function states(): HasMany
101
     {
101
     {
102
         return self::query()
102
         return self::query()
103
             ->select(['id', 'name', 'flag'])
103
             ->select(['id', 'name', 'flag'])
104
-            ->whereLike('name', "%{$search}%")
105
-            ->orWhereLike('id', "%{$search}%")
104
+            ->where(static function ($query) use ($search) {
105
+                $query->whereLike('name', "%{$search}%")
106
+                    ->orWhereLike('id', "%{$search}%");
107
+            })
106
             ->orderByRaw('
108
             ->orderByRaw('
107
                 CASE
109
                 CASE
108
                     WHEN id = ? THEN 1
110
                     WHEN id = ? THEN 1
113
             ', [$search, $search . '%', $search . '%'])
115
             ', [$search, $search . '%', $search . '%'])
114
             ->limit(50)
116
             ->limit(50)
115
             ->get()
117
             ->get()
116
-            ->mapWithKeys(static fn ($country) => [
118
+            ->mapWithKeys(static fn (self $country) => [
117
                 $country->id => $country->name . ' ' . $country->flag,
119
                 $country->id => $country->name . ' ' . $country->flag,
118
             ])
120
             ])
119
             ->toArray();
121
             ->toArray();
121
 
123
 
122
     public static function getLanguagesByCountryCode(?string $code = null): array
124
     public static function getLanguagesByCountryCode(?string $code = null): array
123
     {
125
     {
124
-        if ($code === null) {
126
+        if (! $code) {
125
             return Locales::getNames();
127
             return Locales::getNames();
126
         }
128
         }
127
 
129
 

+ 35
- 5
app/Models/Locale/State.php View File

4
 
4
 
5
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
5
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
6
 use Illuminate\Database\Eloquent\Relations\HasMany;
6
 use Illuminate\Database\Eloquent\Relations\HasMany;
7
-use Illuminate\Support\Collection;
8
 use Squire\Model;
7
 use Squire\Model;
9
 
8
 
10
 /**
9
 /**
28
         'longitude' => 'float',
27
         'longitude' => 'float',
29
     ];
28
     ];
30
 
29
 
31
-    public static function getStateOptions(?string $code = null): Collection
30
+    public static function getStateOptions(?string $code = null): array
32
     {
31
     {
33
-        if ($code === null) {
34
-            return collect();
32
+        if (! $code) {
33
+            return [];
35
         }
34
         }
36
 
35
 
37
-        return self::where('country_id', $code)->get()->pluck('name', 'id');
36
+        return self::query()
37
+            ->where('country_id', $code)
38
+            ->orderBy('name')
39
+            ->get()
40
+            ->pluck('name', 'id')
41
+            ->toArray();
42
+    }
43
+
44
+    public static function getSearchResultsUsing(string $search, ?string $countryCode = null): array
45
+    {
46
+        if (! $countryCode) {
47
+            return [];
48
+        }
49
+
50
+        return self::query()
51
+            ->where('country_id', $countryCode)
52
+            ->where(static function ($query) use ($search) {
53
+                $query->whereLike('name', "%{$search}%")
54
+                    ->orWhereLike('state_code', "%{$search}%");
55
+            })
56
+            ->orderByRaw('
57
+                CASE
58
+                    WHEN state_code = ? THEN 1
59
+                    WHEN state_code LIKE ? THEN 2
60
+                    WHEN name LIKE ? THEN 3
61
+                    ELSE 4
62
+                END
63
+            ', [$search, $search . '%', $search . '%'])
64
+            ->limit(50)
65
+            ->get()
66
+            ->pluck('name', 'id')
67
+            ->toArray();
38
     }
68
     }
39
 
69
 
40
     public function country(): BelongsTo
70
     public function country(): BelongsTo

+ 13
- 2
app/Providers/MacroServiceProvider.php View File

211
                 }
211
                 }
212
 
212
 
213
                 $currency = $column->evaluate($currency);
213
                 $currency = $column->evaluate($currency);
214
+                $showCurrency = $currency !== CurrencyAccessor::getDefaultCurrency();
214
 
215
 
215
                 if ($convertFromCents) {
216
                 if ($convertFromCents) {
216
-                    return CurrencyConverter::formatCentsToMoney($state, $currency);
217
+                    $balanceInCents = $state;
218
+                } else {
219
+                    $balanceInCents = CurrencyConverter::convertToCents($state, $currency);
217
                 }
220
                 }
218
 
221
 
219
-                return CurrencyConverter::formatToMoney($state, $currency);
222
+                if ($balanceInCents < 0) {
223
+                    return '(' . CurrencyConverter::formatCentsToMoney(abs($balanceInCents), $currency, $showCurrency) . ')';
224
+                }
225
+
226
+                return CurrencyConverter::formatCentsToMoney($balanceInCents, $currency, $showCurrency);
220
             });
227
             });
221
 
228
 
222
             $this->description(static function (TextColumn $column, $state) use ($currency, $convertFromCents): ?string {
229
             $this->description(static function (TextColumn $column, $state) use ($currency, $convertFromCents): ?string {
239
 
246
 
240
                 $convertedBalanceInCents = CurrencyConverter::convertBalance($balanceInCents, $oldCurrency, $newCurrency);
247
                 $convertedBalanceInCents = CurrencyConverter::convertBalance($balanceInCents, $oldCurrency, $newCurrency);
241
 
248
 
249
+                if ($convertedBalanceInCents < 0) {
250
+                    return '(' . CurrencyConverter::formatCentsToMoney(abs($convertedBalanceInCents), $newCurrency, true) . ')';
251
+                }
252
+
242
                 return CurrencyConverter::formatCentsToMoney($convertedBalanceInCents, $newCurrency, true);
253
                 return CurrencyConverter::formatCentsToMoney($convertedBalanceInCents, $newCurrency, true);
243
             });
254
             });
244
 
255
 

+ 8
- 2
app/Utilities/Currency/CurrencyConverter.php View File

62
         return $money->format();
62
         return $money->format();
63
     }
63
     }
64
 
64
 
65
-    public static function formatToMoney(string | float $amount, ?string $currency = null): string
65
+    public static function formatToMoney(string | float $amount, ?string $currency = null, bool $withCode = false): string
66
     {
66
     {
67
         $currency ??= CurrencyAccessor::getDefaultCurrency();
67
         $currency ??= CurrencyAccessor::getDefaultCurrency();
68
 
68
 
69
-        return money($amount, $currency, true)->format();
69
+        $money = money($amount, $currency, true);
70
+
71
+        if ($withCode) {
72
+            return $money->formatWithCode();
73
+        }
74
+
75
+        return $money->format();
70
     }
76
     }
71
 
77
 
72
     public static function convertCentsToFloat(int $amount, ?string $currency = null): float
78
     public static function convertCentsToFloat(int $amount, ?string $currency = null): float

+ 23
- 23
composer.lock View File

1029
         },
1029
         },
1030
         {
1030
         {
1031
             "name": "codewithdennis/filament-simple-alert",
1031
             "name": "codewithdennis/filament-simple-alert",
1032
-            "version": "v3.0.15",
1032
+            "version": "v3.0.16",
1033
             "source": {
1033
             "source": {
1034
                 "type": "git",
1034
                 "type": "git",
1035
                 "url": "https://github.com/CodeWithDennis/filament-simple-alert.git",
1035
                 "url": "https://github.com/CodeWithDennis/filament-simple-alert.git",
1036
-                "reference": "8fd7dfa48bb98061bcc3f5fbaacb82dce7a09c7b"
1036
+                "reference": "f29677d3a0d2b6fd9b1c3627152cd0107d2db337"
1037
             },
1037
             },
1038
             "dist": {
1038
             "dist": {
1039
                 "type": "zip",
1039
                 "type": "zip",
1040
-                "url": "https://api.github.com/repos/CodeWithDennis/filament-simple-alert/zipball/8fd7dfa48bb98061bcc3f5fbaacb82dce7a09c7b",
1041
-                "reference": "8fd7dfa48bb98061bcc3f5fbaacb82dce7a09c7b",
1040
+                "url": "https://api.github.com/repos/CodeWithDennis/filament-simple-alert/zipball/f29677d3a0d2b6fd9b1c3627152cd0107d2db337",
1041
+                "reference": "f29677d3a0d2b6fd9b1c3627152cd0107d2db337",
1042
                 "shasum": ""
1042
                 "shasum": ""
1043
             },
1043
             },
1044
             "require": {
1044
             "require": {
1098
                     "type": "github"
1098
                     "type": "github"
1099
                 }
1099
                 }
1100
             ],
1100
             ],
1101
-            "time": "2024-12-03T16:17:47+00:00"
1101
+            "time": "2025-01-19T17:37:34+00:00"
1102
         },
1102
         },
1103
         {
1103
         {
1104
             "name": "danharrin/date-format-converter",
1104
             "name": "danharrin/date-format-converter",
10149
         },
10149
         },
10150
         {
10150
         {
10151
             "name": "pestphp/pest",
10151
             "name": "pestphp/pest",
10152
-            "version": "v3.7.1",
10152
+            "version": "v3.7.2",
10153
             "source": {
10153
             "source": {
10154
                 "type": "git",
10154
                 "type": "git",
10155
                 "url": "https://github.com/pestphp/pest.git",
10155
                 "url": "https://github.com/pestphp/pest.git",
10156
-                "reference": "bf3178473dcaa53b0458f21dfdb271306ea62512"
10156
+                "reference": "709ecb1ba2641fc0c4653ebe1fd8a402bbf4d18b"
10157
             },
10157
             },
10158
             "dist": {
10158
             "dist": {
10159
                 "type": "zip",
10159
                 "type": "zip",
10160
-                "url": "https://api.github.com/repos/pestphp/pest/zipball/bf3178473dcaa53b0458f21dfdb271306ea62512",
10161
-                "reference": "bf3178473dcaa53b0458f21dfdb271306ea62512",
10160
+                "url": "https://api.github.com/repos/pestphp/pest/zipball/709ecb1ba2641fc0c4653ebe1fd8a402bbf4d18b",
10161
+                "reference": "709ecb1ba2641fc0c4653ebe1fd8a402bbf4d18b",
10162
                 "shasum": ""
10162
                 "shasum": ""
10163
             },
10163
             },
10164
             "require": {
10164
             "require": {
10169
                 "pestphp/pest-plugin-arch": "^3.0.0",
10169
                 "pestphp/pest-plugin-arch": "^3.0.0",
10170
                 "pestphp/pest-plugin-mutate": "^3.0.5",
10170
                 "pestphp/pest-plugin-mutate": "^3.0.5",
10171
                 "php": "^8.2.0",
10171
                 "php": "^8.2.0",
10172
-                "phpunit/phpunit": "^11.5.1"
10172
+                "phpunit/phpunit": "^11.5.3"
10173
             },
10173
             },
10174
             "conflict": {
10174
             "conflict": {
10175
                 "filp/whoops": "<2.16.0",
10175
                 "filp/whoops": "<2.16.0",
10176
-                "phpunit/phpunit": ">11.5.1",
10176
+                "phpunit/phpunit": ">11.5.3",
10177
                 "sebastian/exporter": "<6.0.0",
10177
                 "sebastian/exporter": "<6.0.0",
10178
                 "webmozart/assert": "<1.11.0"
10178
                 "webmozart/assert": "<1.11.0"
10179
             },
10179
             },
10180
             "require-dev": {
10180
             "require-dev": {
10181
                 "pestphp/pest-dev-tools": "^3.3.0",
10181
                 "pestphp/pest-dev-tools": "^3.3.0",
10182
-                "pestphp/pest-plugin-type-coverage": "^3.2.0",
10182
+                "pestphp/pest-plugin-type-coverage": "^3.2.3",
10183
                 "symfony/process": "^7.2.0"
10183
                 "symfony/process": "^7.2.0"
10184
             },
10184
             },
10185
             "bin": [
10185
             "bin": [
10245
             ],
10245
             ],
10246
             "support": {
10246
             "support": {
10247
                 "issues": "https://github.com/pestphp/pest/issues",
10247
                 "issues": "https://github.com/pestphp/pest/issues",
10248
-                "source": "https://github.com/pestphp/pest/tree/v3.7.1"
10248
+                "source": "https://github.com/pestphp/pest/tree/v3.7.2"
10249
             },
10249
             },
10250
             "funding": [
10250
             "funding": [
10251
                 {
10251
                 {
10257
                     "type": "github"
10257
                     "type": "github"
10258
                 }
10258
                 }
10259
             ],
10259
             ],
10260
-            "time": "2024-12-12T11:52:01+00:00"
10260
+            "time": "2025-01-19T17:35:09+00:00"
10261
         },
10261
         },
10262
         {
10262
         {
10263
             "name": "pestphp/pest-plugin",
10263
             "name": "pestphp/pest-plugin",
11202
         },
11202
         },
11203
         {
11203
         {
11204
             "name": "phpunit/phpunit",
11204
             "name": "phpunit/phpunit",
11205
-            "version": "11.5.1",
11205
+            "version": "11.5.3",
11206
             "source": {
11206
             "source": {
11207
                 "type": "git",
11207
                 "type": "git",
11208
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
11208
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
11209
-                "reference": "2b94d4f2450b9869fa64a46fd8a6a41997aef56a"
11209
+                "reference": "30e319e578a7b5da3543073e30002bf82042f701"
11210
             },
11210
             },
11211
             "dist": {
11211
             "dist": {
11212
                 "type": "zip",
11212
                 "type": "zip",
11213
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/2b94d4f2450b9869fa64a46fd8a6a41997aef56a",
11214
-                "reference": "2b94d4f2450b9869fa64a46fd8a6a41997aef56a",
11213
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/30e319e578a7b5da3543073e30002bf82042f701",
11214
+                "reference": "30e319e578a7b5da3543073e30002bf82042f701",
11215
                 "shasum": ""
11215
                 "shasum": ""
11216
             },
11216
             },
11217
             "require": {
11217
             "require": {
11225
                 "phar-io/manifest": "^2.0.4",
11225
                 "phar-io/manifest": "^2.0.4",
11226
                 "phar-io/version": "^3.2.1",
11226
                 "phar-io/version": "^3.2.1",
11227
                 "php": ">=8.2",
11227
                 "php": ">=8.2",
11228
-                "phpunit/php-code-coverage": "^11.0.7",
11228
+                "phpunit/php-code-coverage": "^11.0.8",
11229
                 "phpunit/php-file-iterator": "^5.1.0",
11229
                 "phpunit/php-file-iterator": "^5.1.0",
11230
                 "phpunit/php-invoker": "^5.0.1",
11230
                 "phpunit/php-invoker": "^5.0.1",
11231
                 "phpunit/php-text-template": "^4.0.1",
11231
                 "phpunit/php-text-template": "^4.0.1",
11232
                 "phpunit/php-timer": "^7.0.1",
11232
                 "phpunit/php-timer": "^7.0.1",
11233
                 "sebastian/cli-parser": "^3.0.2",
11233
                 "sebastian/cli-parser": "^3.0.2",
11234
-                "sebastian/code-unit": "^3.0.1",
11235
-                "sebastian/comparator": "^6.2.1",
11234
+                "sebastian/code-unit": "^3.0.2",
11235
+                "sebastian/comparator": "^6.3.0",
11236
                 "sebastian/diff": "^6.0.2",
11236
                 "sebastian/diff": "^6.0.2",
11237
                 "sebastian/environment": "^7.2.0",
11237
                 "sebastian/environment": "^7.2.0",
11238
                 "sebastian/exporter": "^6.3.0",
11238
                 "sebastian/exporter": "^6.3.0",
11283
             "support": {
11283
             "support": {
11284
                 "issues": "https://github.com/sebastianbergmann/phpunit/issues",
11284
                 "issues": "https://github.com/sebastianbergmann/phpunit/issues",
11285
                 "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
11285
                 "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
11286
-                "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.1"
11286
+                "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.3"
11287
             },
11287
             },
11288
             "funding": [
11288
             "funding": [
11289
                 {
11289
                 {
11299
                     "type": "tidelift"
11299
                     "type": "tidelift"
11300
                 }
11300
                 }
11301
             ],
11301
             ],
11302
-            "time": "2024-12-11T10:52:48+00:00"
11302
+            "time": "2025-01-13T09:36:00+00:00"
11303
         },
11303
         },
11304
         {
11304
         {
11305
             "name": "pimple/pimple",
11305
             "name": "pimple/pimple",

+ 5
- 5
database/factories/Accounting/RecurringInvoiceFactory.php View File

90
         });
90
         });
91
     }
91
     }
92
 
92
 
93
-    protected function withDailySchedule(Carbon $startDate, EndType $endType): static
93
+    public function withDailySchedule(Carbon $startDate, EndType $endType): static
94
     {
94
     {
95
         return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($startDate, $endType) {
95
         return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($startDate, $endType) {
96
             $this->ensureLineItems($recurringInvoice);
96
             $this->ensureLineItems($recurringInvoice);
103
         });
103
         });
104
     }
104
     }
105
 
105
 
106
-    protected function withWeeklySchedule(Carbon $startDate, EndType $endType): static
106
+    public function withWeeklySchedule(Carbon $startDate, EndType $endType): static
107
     {
107
     {
108
         return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($startDate, $endType) {
108
         return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($startDate, $endType) {
109
             $this->ensureLineItems($recurringInvoice);
109
             $this->ensureLineItems($recurringInvoice);
117
         });
117
         });
118
     }
118
     }
119
 
119
 
120
-    protected function withMonthlySchedule(Carbon $startDate, EndType $endType): static
120
+    public function withMonthlySchedule(Carbon $startDate, EndType $endType): static
121
     {
121
     {
122
         return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($startDate, $endType) {
122
         return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($startDate, $endType) {
123
             $this->ensureLineItems($recurringInvoice);
123
             $this->ensureLineItems($recurringInvoice);
131
         });
131
         });
132
     }
132
     }
133
 
133
 
134
-    protected function withYearlySchedule(Carbon $startDate, EndType $endType): static
134
+    public function withYearlySchedule(Carbon $startDate, EndType $endType): static
135
     {
135
     {
136
         return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($startDate, $endType) {
136
         return $this->afterCreating(function (RecurringInvoice $recurringInvoice) use ($startDate, $endType) {
137
             $this->ensureLineItems($recurringInvoice);
137
             $this->ensureLineItems($recurringInvoice);
146
         });
146
         });
147
     }
147
     }
148
 
148
 
149
-    protected function withCustomSchedule(
149
+    public function withCustomSchedule(
150
         Carbon $startDate,
150
         Carbon $startDate,
151
         EndType $endType,
151
         EndType $endType,
152
         ?IntervalType $intervalType = null,
152
         ?IntervalType $intervalType = null,

+ 1
- 1
database/factories/Common/AddressFactory.php View File

33
             'city' => $this->faker->city,
33
             'city' => $this->faker->city,
34
             'state_id' => $this->faker->state('US'),
34
             'state_id' => $this->faker->state('US'),
35
             'postal_code' => $this->faker->postcode,
35
             'postal_code' => $this->faker->postcode,
36
-            'country' => 'US',
36
+            'country_code' => 'US',
37
             'notes' => $this->faker->sentence,
37
             'notes' => $this->faker->sentence,
38
             'created_by' => 1,
38
             'created_by' => 1,
39
             'updated_by' => 1,
39
             'updated_by' => 1,

+ 1
- 1
database/factories/CompanyFactory.php View File

52
     public function withCompanyDefaults(): self
52
     public function withCompanyDefaults(): self
53
     {
53
     {
54
         return $this->afterCreating(function (Company $company) {
54
         return $this->afterCreating(function (Company $company) {
55
-            $countryCode = $company->profile->address->country;
55
+            $countryCode = $company->profile->address->country_code;
56
             $companyDefaultService = app(CompanyDefaultService::class);
56
             $companyDefaultService = app(CompanyDefaultService::class);
57
             $companyDefaultService->createCompanyDefaults($company, $company->owner, 'USD', $countryCode, 'en');
57
             $companyDefaultService->createCompanyDefaults($company, $company->owner, 'USD', $countryCode, 'en');
58
         });
58
         });

+ 1
- 1
database/migrations/2024_11_19_225812_create_addresses_table.php View File

24
             $table->string('city')->nullable();
24
             $table->string('city')->nullable();
25
             $table->smallInteger('state_id')->nullable();
25
             $table->smallInteger('state_id')->nullable();
26
             $table->string('postal_code')->nullable();
26
             $table->string('postal_code')->nullable();
27
-            $table->string('country');
27
+            $table->string('country_code');
28
             $table->text('notes')->nullable();
28
             $table->text('notes')->nullable();
29
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
29
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
30
             $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
30
             $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();

+ 79
- 79
package-lock.json View File

575
             }
575
             }
576
         },
576
         },
577
         "node_modules/@rollup/rollup-android-arm-eabi": {
577
         "node_modules/@rollup/rollup-android-arm-eabi": {
578
-            "version": "4.30.1",
579
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.30.1.tgz",
580
-            "integrity": "sha512-pSWY+EVt3rJ9fQ3IqlrEUtXh3cGqGtPDH1FQlNZehO2yYxCHEX1SPsz1M//NXwYfbTlcKr9WObLnJX9FsS9K1Q==",
578
+            "version": "4.31.0",
579
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.31.0.tgz",
580
+            "integrity": "sha512-9NrR4033uCbUBRgvLcBrJofa2KY9DzxL2UKZ1/4xA/mnTNyhZCWBuD8X3tPm1n4KxcgaraOYgrFKSgwjASfmlA==",
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.30.1",
593
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.30.1.tgz",
594
-            "integrity": "sha512-/NA2qXxE3D/BRjOJM8wQblmArQq1YoBVJjrjoTSBS09jgUisq7bqxNHJ8kjCHeV21W/9WDGwJEWSN0KQ2mtD/w==",
592
+            "version": "4.31.0",
593
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.31.0.tgz",
594
+            "integrity": "sha512-iBbODqT86YBFHajxxF8ebj2hwKm1k8PTBQSojSt3d1FFt1gN+xf4CowE47iN0vOSdnd+5ierMHBbu/rHc7nq5g==",
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.30.1",
607
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.30.1.tgz",
608
-            "integrity": "sha512-r7FQIXD7gB0WJ5mokTUgUWPl0eYIH0wnxqeSAhuIwvnnpjdVB8cRRClyKLQr7lgzjctkbp5KmswWszlwYln03Q==",
606
+            "version": "4.31.0",
607
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.31.0.tgz",
608
+            "integrity": "sha512-WHIZfXgVBX30SWuTMhlHPXTyN20AXrLH4TEeH/D0Bolvx9PjgZnn4H677PlSGvU6MKNsjCQJYczkpvBbrBnG6g==",
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.30.1",
621
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.30.1.tgz",
622
-            "integrity": "sha512-x78BavIwSH6sqfP2xeI1hd1GpHL8J4W2BXcVM/5KYKoAD3nNsfitQhvWSw+TFtQTLZ9OmlF+FEInEHyubut2OA==",
620
+            "version": "4.31.0",
621
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.31.0.tgz",
622
+            "integrity": "sha512-hrWL7uQacTEF8gdrQAqcDy9xllQ0w0zuL1wk1HV8wKGSGbKPVjVUv/DEwT2+Asabf8Dh/As+IvfdU+H8hhzrQQ==",
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.30.1",
635
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.30.1.tgz",
636
-            "integrity": "sha512-HYTlUAjbO1z8ywxsDFWADfTRfTIIy/oUlfIDmlHYmjUP2QRDTzBuWXc9O4CXM+bo9qfiCclmHk1x4ogBjOUpUQ==",
634
+            "version": "4.31.0",
635
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.31.0.tgz",
636
+            "integrity": "sha512-S2oCsZ4hJviG1QjPY1h6sVJLBI6ekBeAEssYKad1soRFv3SocsQCzX6cwnk6fID6UQQACTjeIMB+hyYrFacRew==",
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.30.1",
649
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.30.1.tgz",
650
-            "integrity": "sha512-1MEdGqogQLccphhX5myCJqeGNYTNcmTyaic9S7CG3JhwuIByJ7J05vGbZxsizQthP1xpVx7kd3o31eOogfEirw==",
648
+            "version": "4.31.0",
649
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.31.0.tgz",
650
+            "integrity": "sha512-pCANqpynRS4Jirn4IKZH4tnm2+2CqCNLKD7gAdEjzdLGbH1iO0zouHz4mxqg0uEMpO030ejJ0aA6e1PJo2xrPA==",
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.30.1",
663
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.30.1.tgz",
664
-            "integrity": "sha512-PaMRNBSqCx7K3Wc9QZkFx5+CX27WFpAMxJNiYGAXfmMIKC7jstlr32UhTgK6T07OtqR+wYlWm9IxzennjnvdJg==",
662
+            "version": "4.31.0",
663
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.31.0.tgz",
664
+            "integrity": "sha512-0O8ViX+QcBd3ZmGlcFTnYXZKGbFu09EhgD27tgTdGnkcYXLat4KIsBBQeKLR2xZDCXdIBAlWLkiXE1+rJpCxFw==",
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.30.1",
677
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.30.1.tgz",
678
-            "integrity": "sha512-B8Rcyj9AV7ZlEFqvB5BubG5iO6ANDsRKlhIxySXcF1axXYUyqwBok+XZPgIYGBgs7LDXfWfifxhw0Ik57T0Yug==",
676
+            "version": "4.31.0",
677
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.31.0.tgz",
678
+            "integrity": "sha512-w5IzG0wTVv7B0/SwDnMYmbr2uERQp999q8FMkKG1I+j8hpPX2BYFjWe69xbhbP6J9h2gId/7ogesl9hwblFwwg==",
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.30.1",
691
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.30.1.tgz",
692
-            "integrity": "sha512-hqVyueGxAj3cBKrAI4aFHLV+h0Lv5VgWZs9CUGqr1z0fZtlADVV1YPOij6AhcK5An33EXaxnDLmJdQikcn5NEw==",
690
+            "version": "4.31.0",
691
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.31.0.tgz",
692
+            "integrity": "sha512-JyFFshbN5xwy6fulZ8B/8qOqENRmDdEkcIMF0Zz+RsfamEW+Zabl5jAb0IozP/8UKnJ7g2FtZZPEUIAlUSX8cA==",
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.30.1",
705
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.30.1.tgz",
706
-            "integrity": "sha512-i4Ab2vnvS1AE1PyOIGp2kXni69gU2DAUVt6FSXeIqUCPIR3ZlheMW3oP2JkukDfu3PsexYRbOiJrY+yVNSk9oA==",
704
+            "version": "4.31.0",
705
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.31.0.tgz",
706
+            "integrity": "sha512-kpQXQ0UPFeMPmPYksiBL9WS/BDiQEjRGMfklVIsA0Sng347H8W2iexch+IEwaR7OVSKtr2ZFxggt11zVIlZ25g==",
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.30.1",
719
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.30.1.tgz",
720
-            "integrity": "sha512-fARcF5g296snX0oLGkVxPmysetwUk2zmHcca+e9ObOovBR++9ZPOhqFUM61UUZ2EYpXVPN1redgqVoBB34nTpQ==",
718
+            "version": "4.31.0",
719
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.31.0.tgz",
720
+            "integrity": "sha512-pMlxLjt60iQTzt9iBb3jZphFIl55a70wexvo8p+vVFK+7ifTRookdoXX3bOsRdmfD+OKnMozKO6XM4zR0sHRrQ==",
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.30.1",
733
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.30.1.tgz",
734
-            "integrity": "sha512-GLrZraoO3wVT4uFXh67ElpwQY0DIygxdv0BNW9Hkm3X34wu+BkqrDrkcsIapAY+N2ATEbvak0XQ9gxZtCIA5Rw==",
732
+            "version": "4.31.0",
733
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.31.0.tgz",
734
+            "integrity": "sha512-D7TXT7I/uKEuWiRkEFbed1UUYZwcJDU4vZQdPTcepK7ecPhzKOYk4Er2YR4uHKme4qDeIh6N3XrLfpuM7vzRWQ==",
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.30.1",
747
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.30.1.tgz",
748
-            "integrity": "sha512-0WKLaAUUHKBtll0wvOmh6yh3S0wSU9+yas923JIChfxOaaBarmb/lBKPF0w/+jTVozFnOXJeRGZ8NvOxvk/jcw==",
746
+            "version": "4.31.0",
747
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.31.0.tgz",
748
+            "integrity": "sha512-wal2Tc8O5lMBtoePLBYRKj2CImUCJ4UNGJlLwspx7QApYny7K1cUYlzQ/4IGQBLmm+y0RS7dwc3TDO/pmcneTw==",
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.30.1",
761
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.30.1.tgz",
762
-            "integrity": "sha512-GWFs97Ruxo5Bt+cvVTQkOJ6TIx0xJDD/bMAOXWJg8TCSTEK8RnFeOeiFTxKniTc4vMIaWvCplMAFBt9miGxgkA==",
760
+            "version": "4.31.0",
761
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.31.0.tgz",
762
+            "integrity": "sha512-O1o5EUI0+RRMkK9wiTVpk2tyzXdXefHtRTIjBbmFREmNMy7pFeYXCFGbhKFwISA3UOExlo5GGUuuj3oMKdK6JQ==",
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.30.1",
775
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.30.1.tgz",
776
-            "integrity": "sha512-UtgGb7QGgXDIO+tqqJ5oZRGHsDLO8SlpE4MhqpY9Llpzi5rJMvrK6ZGhsRCST2abZdBqIBeXW6WPD5fGK5SDwg==",
774
+            "version": "4.31.0",
775
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.31.0.tgz",
776
+            "integrity": "sha512-zSoHl356vKnNxwOWnLd60ixHNPRBglxpv2g7q0Cd3Pmr561gf0HiAcUBRL3S1vPqRC17Zo2CX/9cPkqTIiai1g==",
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.30.1",
789
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.30.1.tgz",
790
-            "integrity": "sha512-V9U8Ey2UqmQsBT+xTOeMzPzwDzyXmnAoO4edZhL7INkwQcaW1Ckv3WJX3qrrp/VHaDkEWIBWhRwP47r8cdrOow==",
788
+            "version": "4.31.0",
789
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.31.0.tgz",
790
+            "integrity": "sha512-ypB/HMtcSGhKUQNiFwqgdclWNRrAYDH8iMYH4etw/ZlGwiTVxBz2tDrGRrPlfZu6QjXwtd+C3Zib5pFqID97ZA==",
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.30.1",
803
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.30.1.tgz",
804
-            "integrity": "sha512-WabtHWiPaFF47W3PkHnjbmWawnX/aE57K47ZDT1BXTS5GgrBUEpvOzq0FI0V/UYzQJgdb8XlhVNH8/fwV8xDjw==",
802
+            "version": "4.31.0",
803
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.31.0.tgz",
804
+            "integrity": "sha512-JuhN2xdI/m8Hr+aVO3vspO7OQfUFO6bKLIRTAy0U15vmWjnZDLrEgCZ2s6+scAYaQVpYSh9tZtRijApw9IXyMw==",
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.30.1",
817
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.30.1.tgz",
818
-            "integrity": "sha512-pxHAU+Zv39hLUTdQQHUVHf4P+0C47y/ZloorHpzs2SXMRqeAWmGghzAhfOlzFHHwjvgokdFAhC4V+6kC1lRRfw==",
816
+            "version": "4.31.0",
817
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.31.0.tgz",
818
+            "integrity": "sha512-U1xZZXYkvdf5MIWmftU8wrM5PPXzyaY1nGCI4KI4BFfoZxHamsIe+BtnPLIvvPykvQWlVbqUXdLa4aJUuilwLQ==",
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.30.1",
831
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.30.1.tgz",
832
-            "integrity": "sha512-D6qjsXGcvhTjv0kI4fU8tUuBDF/Ueee4SVX79VfNDXZa64TfCW1Slkb6Z7O1p7vflqZjcmOVdZlqf8gvJxc6og==",
830
+            "version": "4.31.0",
831
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.31.0.tgz",
832
+            "integrity": "sha512-ul8rnCsUumNln5YWwz0ted2ZHFhzhRRnkpBZ+YRuHoRAlUji9KChpOUOndY7uykrPEPXVbHLlsdo6v5yXo/TXw==",
833
             "cpu": [
833
             "cpu": [
834
                 "x64"
834
                 "x64"
835
             ],
835
             ],
2242
             }
2242
             }
2243
         },
2243
         },
2244
         "node_modules/rollup": {
2244
         "node_modules/rollup": {
2245
-            "version": "4.30.1",
2246
-            "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.30.1.tgz",
2247
-            "integrity": "sha512-mlJ4glW020fPuLi7DkM/lN97mYEZGWeqBnrljzN0gs7GLctqX3lNWxKQ7Gl712UAX+6fog/L3jh4gb7R6aVi3w==",
2245
+            "version": "4.31.0",
2246
+            "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.31.0.tgz",
2247
+            "integrity": "sha512-9cCE8P4rZLx9+PjoyqHLs31V9a9Vpvfo4qNcs6JCiGWYhw2gijSetFbH6SSy1whnkgcefnUwr8sad7tgqsGvnw==",
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.30.1",
2262
-                "@rollup/rollup-android-arm64": "4.30.1",
2263
-                "@rollup/rollup-darwin-arm64": "4.30.1",
2264
-                "@rollup/rollup-darwin-x64": "4.30.1",
2265
-                "@rollup/rollup-freebsd-arm64": "4.30.1",
2266
-                "@rollup/rollup-freebsd-x64": "4.30.1",
2267
-                "@rollup/rollup-linux-arm-gnueabihf": "4.30.1",
2268
-                "@rollup/rollup-linux-arm-musleabihf": "4.30.1",
2269
-                "@rollup/rollup-linux-arm64-gnu": "4.30.1",
2270
-                "@rollup/rollup-linux-arm64-musl": "4.30.1",
2271
-                "@rollup/rollup-linux-loongarch64-gnu": "4.30.1",
2272
-                "@rollup/rollup-linux-powerpc64le-gnu": "4.30.1",
2273
-                "@rollup/rollup-linux-riscv64-gnu": "4.30.1",
2274
-                "@rollup/rollup-linux-s390x-gnu": "4.30.1",
2275
-                "@rollup/rollup-linux-x64-gnu": "4.30.1",
2276
-                "@rollup/rollup-linux-x64-musl": "4.30.1",
2277
-                "@rollup/rollup-win32-arm64-msvc": "4.30.1",
2278
-                "@rollup/rollup-win32-ia32-msvc": "4.30.1",
2279
-                "@rollup/rollup-win32-x64-msvc": "4.30.1",
2261
+                "@rollup/rollup-android-arm-eabi": "4.31.0",
2262
+                "@rollup/rollup-android-arm64": "4.31.0",
2263
+                "@rollup/rollup-darwin-arm64": "4.31.0",
2264
+                "@rollup/rollup-darwin-x64": "4.31.0",
2265
+                "@rollup/rollup-freebsd-arm64": "4.31.0",
2266
+                "@rollup/rollup-freebsd-x64": "4.31.0",
2267
+                "@rollup/rollup-linux-arm-gnueabihf": "4.31.0",
2268
+                "@rollup/rollup-linux-arm-musleabihf": "4.31.0",
2269
+                "@rollup/rollup-linux-arm64-gnu": "4.31.0",
2270
+                "@rollup/rollup-linux-arm64-musl": "4.31.0",
2271
+                "@rollup/rollup-linux-loongarch64-gnu": "4.31.0",
2272
+                "@rollup/rollup-linux-powerpc64le-gnu": "4.31.0",
2273
+                "@rollup/rollup-linux-riscv64-gnu": "4.31.0",
2274
+                "@rollup/rollup-linux-s390x-gnu": "4.31.0",
2275
+                "@rollup/rollup-linux-x64-gnu": "4.31.0",
2276
+                "@rollup/rollup-linux-x64-musl": "4.31.0",
2277
+                "@rollup/rollup-win32-arm64-msvc": "4.31.0",
2278
+                "@rollup/rollup-win32-ia32-msvc": "4.31.0",
2279
+                "@rollup/rollup-win32-x64-msvc": "4.31.0",
2280
                 "fsevents": "~2.3.2"
2280
                 "fsevents": "~2.3.2"
2281
             }
2281
             }
2282
         },
2282
         },

+ 4
- 17
resources/views/filament/infolists/components/document-preview.blade.php View File

26
         <x-company.invoice.metadata class="modern-template-metadata space-y-8">
26
         <x-company.invoice.metadata class="modern-template-metadata space-y-8">
27
             <div class="text-sm">
27
             <div class="text-sm">
28
                 <h2 class="text-lg font-semibold">{{ $document->company->name }}</h2>
28
                 <h2 class="text-lg font-semibold">{{ $document->company->name }}</h2>
29
-                @if($document->company->address && $document->company->city && $document->company->state && $document->company->zipCode)
30
-                    <p>{{ $document->company->address }}</p>
31
-                    <p>{{ $document->company->city }}, {{ $document->company->state }} {{ $document->company->zipCode }}</p>
32
-                    <p>{{ $document->company->country }}</p>
29
+                @if($formattedAddress = $document->company->getFormattedAddressHtml())
30
+                    {!! $formattedAddress !!}
33
                 @endif
31
                 @endif
34
             </div>
32
             </div>
35
 
33
 
40
                     <p class="text-base font-bold"
38
                     <p class="text-base font-bold"
41
                        style="color: {{ $document->accentColor }}">{{ $document->client->name }}</p>
39
                        style="color: {{ $document->accentColor }}">{{ $document->client->name }}</p>
42
 
40
 
43
-                    @if($document->client->addressLine1)
44
-                        <p>{{ $document->client->addressLine1 }}</p>
45
-
46
-                        @if($document->client->addressLine2)
47
-                            <p>{{ $document->client->addressLine2 }}</p>
48
-                        @endif
49
-                        <p>
50
-                            {{ $document->client->city }}{{ $document->client->state ? ', ' . $document->client->state: '' }}
51
-                            {{ $document->client->postalCode }}
52
-                        </p>
53
-                        @if($document->client->country)
54
-                            <p>{{ $document->client->country }}</p>
55
-                        @endif
41
+                    @if($formattedAddress = $document->client->getFormattedAddressHtml())
42
+                        {!! $formattedAddress !!}
56
                     @endif
43
                     @endif
57
                 </div>
44
                 </div>
58
 
45
 

+ 0
- 0
resources/views/vendor/filament-simple-alert/.gitkeep View File


+ 0
- 15
resources/views/vendor/filament-simple-alert/components/simple-alert-entry.blade.php View File

1
-<x-dynamic-component :component="$getEntryWrapperView()" :entry="$entry">
2
-    <x-filament-simple-alert::simple-alert
3
-            :icon="$getIcon()"
4
-            :icon-vertical-alignment="$getIconVerticalAlignment()"
5
-            :color="$getColor()"
6
-            :title="$getTitle()"
7
-            :description="$getDescription()"
8
-            :link="$getLink()"
9
-            :link-label="$getLinkLabel()"
10
-            :link-blank="$getLinkBlank()"
11
-            :actions-vertical-alignment="$getActionsVerticalAlignment()"
12
-            :actions="$getActions()"
13
-            :border="$getBorder()"
14
-    />
15
-</x-dynamic-component>

+ 0
- 15
resources/views/vendor/filament-simple-alert/components/simple-alert-field.blade.php View File

1
-<x-dynamic-component :component="$getFieldWrapperView()" :field="$field">
2
-    <x-filament-simple-alert::simple-alert
3
-            :icon="$getIcon()"
4
-            :icon-vertical-alignment="$getIconVerticalAlignment()"
5
-            :color="$getColor()"
6
-            :title="$getTitle()"
7
-            :description="$getDescription()"
8
-            :link="$getLink()"
9
-            :link-label="$getLinkLabel()"
10
-            :link-blank="$getLinkBlank()"
11
-            :actions-vertical-alignment="$getActionsVerticalAlignment()"
12
-            :actions="$getActions()"
13
-            :border="$getBorder()"
14
-    />
15
-</x-dynamic-component>

+ 0
- 84
resources/views/vendor/filament-simple-alert/components/simple-alert.blade.php View File

1
-@props([
2
-    'actions' => null,
3
-    'actionsVerticalAlignment' => 'center',
4
-    'border' => false,
5
-    'color' => null,
6
-    'description' => null,
7
-    'icon' => null,
8
-    'iconVerticalAlignment' => 'center',
9
-    'link' => null,
10
-    'linkBlank' => false,
11
-    'linkLabel' => null,
12
-    'title' => null,
13
-])
14
-
15
-@php
16
-    use function Filament\Support\get_color_css_variables;
17
-
18
-    $colors = \Illuminate\Support\Arr::toCssStyles([
19
-           get_color_css_variables($color, shades: [50, 100, 400, 500, 700, 800]),
20
-   ]);
21
-@endphp
22
-
23
-<div x-data="{}"
24
-     @class([
25
-       'filament-simple-alert rounded-md bg-custom-50 p-4 dark:bg-custom-400/10',
26
-       'ring-1 ring-custom-100 dark:ring-custom-500/70' => $border,
27
-     ])
28
-     style="{{ $colors }}">
29
-    <div class="flex gap-3">
30
-        @if($icon)
31
-            <div @class([
32
-                'flex-shrink-0',
33
-                $iconVerticalAlignment === 'start' ? 'self-start' : 'self-center',
34
-            ])>
35
-                <x-filament::icon
36
-                        :icon="$icon"
37
-                        class="h-5 w-5 text-custom-400"
38
-                />
39
-            </div>
40
-        @endif
41
-        <div class="items-center flex-1 md:flex md:justify-between space-y-3 md:space-y-0 md:gap-3">
42
-            @if($title || $description)
43
-                <div class="space-y-0.5">
44
-                    @if($title)
45
-                        <p class="text-sm font-medium text-custom-800 dark:text-white">
46
-                            {{ $title }}
47
-                        </p>
48
-                    @endif
49
-                    @if($description)
50
-                        <p class="text-sm text-custom-700 dark:text-white">
51
-                            {{ $description }}
52
-                        </p>
53
-                    @endif
54
-                </div>
55
-            @endif
56
-            @if($link || $actions)
57
-                <div @class([
58
-                  'flex items-center gap-3',
59
-                    $actionsVerticalAlignment === 'start' ? 'self-start' : 'self-center',
60
-                ])>
61
-                    <div class="flex items-center whitespace-nowrap gap-3">
62
-                        @if($link)
63
-                            <p class="text-sm md:mt-0 self-center">
64
-                                <a href="{{ $link }}" {{ $linkBlank ? 'target="_blank"' : '' }} class="whitespace-nowrap font-medium text-custom-400 hover:text-custom-500">
65
-                                    {{ $linkLabel }}
66
-                                    <span aria-hidden="true"> &rarr;</span>
67
-                                </a>
68
-                            </p>
69
-                        @endif
70
-                        @if($actions)
71
-                            <div class="gap-3 flex items-center justify-start">
72
-                                @foreach ($actions as $action)
73
-                                    @if ($action->isVisible())
74
-                                        {{ $action }}
75
-                                    @endif
76
-                                @endforeach
77
-                            </div>
78
-                        @endif
79
-                    </div>
80
-                </div>
81
-            @endif
82
-        </div>
83
-    </div>
84
-</div>

+ 187
- 13
tests/Feature/Accounting/RecurringInvoiceTest.php View File

1
 <?php
1
 <?php
2
 
2
 
3
+use App\Enums\Accounting\EndType;
4
+use App\Enums\Accounting\Frequency;
3
 use App\Enums\Accounting\IntervalType;
5
 use App\Enums\Accounting\IntervalType;
4
 use App\Models\Accounting\RecurringInvoice;
6
 use App\Models\Accounting\RecurringInvoice;
7
+use Illuminate\Support\Carbon;
5
 
8
 
6
-test('example', function () {
9
+beforeEach(function () {
10
+    $this->withOfferings();
11
+});
12
+
13
+test('recurring invoice properly handles months with fewer days for monthly frequency', function () {
14
+    // Start from January 31st
15
+    Carbon::setTestNow('2024-01-31');
16
+
17
+    RecurringInvoice::unsetEventDispatcher();
18
+
19
+    // Create a recurring invoice set for the 31st of each month
7
     $recurringInvoice = RecurringInvoice::factory()
20
     $recurringInvoice = RecurringInvoice::factory()
8
-        ->custom(IntervalType::Week, 2)
9
-        ->create([
10
-            'start_date' => today(),
11
-            'day_of_week' => today()->dayOfWeek,
12
-        ]);
21
+        ->withLineItems()
22
+        ->withSchedule(
23
+            frequency: Frequency::Monthly,
24
+            startDate: Carbon::now(),
25
+        )
26
+        ->approved()
27
+        ->create();
28
+
29
+    // First invoice should be the start date
30
+    expect($recurringInvoice->calculateNextDate())
31
+        ->toBeInstanceOf(Carbon::class)
32
+        ->toDateString()->toBe('2024-01-31');
33
+
34
+    // Now set last_date to simulate first invoice being generated
35
+    $recurringInvoice->update(['last_date' => '2024-01-31']);
36
+    $recurringInvoice->refresh();
37
+    expect($recurringInvoice->calculateNextDate())
38
+        ->toBeInstanceOf(Carbon::class)
39
+        ->toDateString()->toBe('2024-02-29');
40
+
41
+    // Update last_date to Feb 29 and check next date (should be March 31)
42
+    $recurringInvoice->update(['last_date' => '2024-02-29']);
43
+    $recurringInvoice->refresh();
44
+    expect($recurringInvoice->calculateNextDate())
45
+        ->toBeInstanceOf(Carbon::class)
46
+        ->toDateString()->toBe('2024-03-31');
47
+
48
+    // Update last_date to March 31 and check next date (should be April 30)
49
+    $recurringInvoice->update(['last_date' => '2024-03-31']);
50
+    $recurringInvoice->refresh();
51
+    expect($recurringInvoice->calculateNextDate())
52
+        ->toBeInstanceOf(Carbon::class)
53
+        ->toDateString()->toBe('2024-04-30');
54
+
55
+    // Update last_date to April 30 and check next date (should be May 31)
56
+    $recurringInvoice->update(['last_date' => '2024-04-30']);
57
+    $recurringInvoice->refresh();
58
+    expect($recurringInvoice->calculateNextDate())
59
+        ->toBeInstanceOf(Carbon::class)
60
+        ->toDateString()->toBe('2024-05-31');
61
+});
62
+
63
+test('recurring invoice properly handles months with fewer days for yearly frequency', function () {
64
+    // Start from January 31st
65
+    Carbon::setTestNow('2024-02-29');
66
+
67
+    RecurringInvoice::unsetEventDispatcher();
68
+
69
+    // Create a recurring invoice set for the 31st of each month
70
+    $recurringInvoice = RecurringInvoice::factory()
71
+        ->withLineItems()
72
+        ->withSchedule(
73
+            frequency: Frequency::Yearly,
74
+            startDate: Carbon::now(),
75
+        )
76
+        ->approved()
77
+        ->create();
78
+
79
+    // First invoice should be the start date
80
+    expect($recurringInvoice->calculateNextDate())
81
+        ->toBeInstanceOf(Carbon::class)
82
+        ->toDateString()->toBe('2024-02-29');
83
+
84
+    // Next date should be Feb 28, 2025 (non-leap year)
85
+    $recurringInvoice->update(['last_date' => '2024-02-29']);
86
+    $recurringInvoice->refresh();
87
+    expect($recurringInvoice->calculateNextDate())
88
+        ->toBeInstanceOf(Carbon::class)
89
+        ->toDateString()->toBe('2025-02-28');
13
 
90
 
91
+    // Next date should be Feb 29, 2026 (leap year)
92
+    $recurringInvoice->update(['last_date' => '2025-02-28']);
14
     $recurringInvoice->refresh();
93
     $recurringInvoice->refresh();
94
+    expect($recurringInvoice->calculateNextDate())
95
+        ->toBeInstanceOf(Carbon::class)
96
+        ->toDateString()->toBe('2026-02-28');
97
+});
98
+
99
+test('recurring invoice properly handles weekly frequency and custom weekly intervals', function () {
100
+    Carbon::setTestNow('2024-01-31'); // Wednesday
15
 
101
 
16
-    $nextInvoiceDate = $recurringInvoice->calculateNextDate();
102
+    RecurringInvoice::unsetEventDispatcher();
17
 
103
 
18
-    expect($nextInvoiceDate)->toEqual(today());
104
+    // Test regular weekly frequency
105
+    $recurringInvoice = RecurringInvoice::factory()
106
+        ->withLineItems()
107
+        ->withSchedule(
108
+            frequency: Frequency::Weekly,
109
+            startDate: Carbon::now(),
110
+        )
111
+        ->approved()
112
+        ->create();
19
 
113
 
20
-    $recurringInvoice->update([
21
-        'last_date' => $nextInvoiceDate,
22
-    ]);
114
+    // First invoice should be the start date
115
+    expect($recurringInvoice->calculateNextDate())
116
+        ->toBeInstanceOf(Carbon::class)
117
+        ->toDateString()->toBe('2024-01-31');
23
 
118
 
119
+    // Next date should be that Friday
120
+    $recurringInvoice->update(['last_date' => '2024-01-31']);
24
     $recurringInvoice->refresh();
121
     $recurringInvoice->refresh();
122
+    expect($recurringInvoice->calculateNextDate())
123
+        ->toBeInstanceOf(Carbon::class)
124
+        ->toDateString()->toBe('2024-02-07');
125
+
126
+    // Test custom weekly frequency (every 2 weeks)
127
+    $recurringInvoice = RecurringInvoice::factory()
128
+        ->withLineItems()
129
+        ->withCustomSchedule(
130
+            startDate: Carbon::now(), // Wednesday
131
+            endType: EndType::Never,
132
+            intervalType: IntervalType::Week,
133
+            intervalValue: 2,
134
+        )
135
+        ->approved()
136
+        ->create();
137
+
138
+    // First invoice should be the start date
139
+    expect($recurringInvoice->calculateNextDate())
140
+        ->toBeInstanceOf(Carbon::class)
141
+        ->toDateString()->toBe('2024-01-31');
142
+
143
+    // Next date should be two weeks from start, on Friday
144
+    $recurringInvoice->update(['last_date' => '2024-01-31']);
145
+    $recurringInvoice->refresh();
146
+    expect($recurringInvoice->calculateNextDate())
147
+        ->toBeInstanceOf(Carbon::class)
148
+        ->toDateString()->toBe('2024-02-14');
149
+});
150
+
151
+test('recurring invoice generates correct sequence of invoices across different month lengths', function () {
152
+    Carbon::setTestNow('2024-01-31');
153
+
154
+    $recurringInvoice = RecurringInvoice::factory()
155
+        ->withLineItems()
156
+        ->withSchedule(
157
+            frequency: Frequency::Monthly,
158
+            startDate: Carbon::now(),
159
+        )
160
+        ->approved()
161
+        ->create();
162
+
163
+    // Generate first invoice
164
+    $recurringInvoice->generateDueInvoices();
165
+
166
+    $invoices = $recurringInvoice->invoices()
167
+        ->orderBy('date')
168
+        ->get();
169
+
170
+    expect($invoices)->toHaveCount(1)
171
+        ->and($invoices->pluck('date')->map->toDateString()->toArray())->toBe([
172
+            '2024-01-31',
173
+        ]);
174
+
175
+    // Move time forward to February (leap year)
176
+    Carbon::setTestNow('2024-02-29');
177
+    $recurringInvoice->generateDueInvoices();
25
 
178
 
26
-    $nextInvoiceDate = $recurringInvoice->calculateNextDate();
179
+    $invoices = $recurringInvoice->invoices()
180
+        ->orderBy('date')
181
+        ->get();
27
 
182
 
28
-    expect($nextInvoiceDate)->toEqual(today()->addWeeks(2));
183
+    expect($invoices)->toHaveCount(2)
184
+        ->and($invoices->pluck('date')->map->toDateString()->toArray())->toBe([
185
+            '2024-01-31',
186
+            '2024-02-29',
187
+        ]);
188
+
189
+    // Move time forward to March
190
+    Carbon::setTestNow('2024-03-31');
191
+    $recurringInvoice->generateDueInvoices();
192
+
193
+    $invoices = $recurringInvoice->invoices()
194
+        ->orderBy('date')
195
+        ->get();
196
+
197
+    expect($invoices)->toHaveCount(3)
198
+        ->and($invoices->pluck('date')->map->toDateString()->toArray())->toBe([
199
+            '2024-01-31',
200
+            '2024-02-29',
201
+            '2024-03-31',
202
+        ]);
29
 });
203
 });

+ 14
- 0
tests/TestCase.php View File

2
 
2
 
3
 namespace Tests;
3
 namespace Tests;
4
 
4
 
5
+use App\Models\Common\Offering;
5
 use App\Models\Company;
6
 use App\Models\Company;
6
 use App\Models\User;
7
 use App\Models\User;
7
 use App\Testing\TestsReport;
8
 use App\Testing\TestsReport;
45
 
46
 
46
         Filament::setTenant($this->testCompany);
47
         Filament::setTenant($this->testCompany);
47
     }
48
     }
49
+
50
+    public function withOfferings(): static
51
+    {
52
+        Offering::factory()
53
+            ->for($this->testCompany)
54
+            ->sellable()
55
+            ->withSalesAdjustments()
56
+            ->purchasable()
57
+            ->withPurchaseAdjustments()
58
+            ->create();
59
+
60
+        return $this;
61
+    }
48
 }
62
 }

Loading…
Cancel
Save