瀏覽代碼

Merge pull request #71 from andrewdwallo/development-3.x

Development 3.x
3.x
Andrew Wallo 1 年之前
父節點
當前提交
4d3f41e0b1
沒有連結到貢獻者的電子郵件帳戶。
共有 38 個檔案被更改,包括 1027 行新增717 行删除
  1. 9
    19
      app/Collections/Accounting/JournalEntryCollection.php
  2. 1
    5
      app/Concerns/CompanyOwned.php
  3. 0
    51
      app/Concerns/HandlesResourceRecordCreation.php
  4. 0
    80
      app/Concerns/HandlesResourceRecordUpdate.php
  5. 28
    0
      app/Concerns/HasDefault.php
  6. 12
    32
      app/Filament/Company/Clusters/Settings/Resources/DiscountResource.php
  7. 0
    30
      app/Filament/Company/Clusters/Settings/Resources/DiscountResource/Pages/CreateDiscount.php
  8. 0
    29
      app/Filament/Company/Clusters/Settings/Resources/DiscountResource/Pages/EditDiscount.php
  9. 9
    7
      app/Filament/Company/Clusters/Settings/Resources/TaxResource.php
  10. 0
    30
      app/Filament/Company/Clusters/Settings/Resources/TaxResource/Pages/CreateTax.php
  11. 0
    29
      app/Filament/Company/Clusters/Settings/Resources/TaxResource/Pages/EditTax.php
  12. 1
    1
      app/Filament/Company/Pages/Accounting/Transactions.php
  13. 3
    1
      app/Filament/Company/Pages/CreateCompany.php
  14. 0
    21
      app/Filament/Company/Resources/Banking/AccountResource/Pages/CreateAccount.php
  15. 0
    20
      app/Filament/Company/Resources/Banking/AccountResource/Pages/EditAccount.php
  16. 2
    0
      app/Models/Setting/Discount.php
  17. 2
    0
      app/Models/Setting/Tax.php
  18. 11
    3
      app/Providers/MacroServiceProvider.php
  19. 0
    42
      app/Repositories/Setting/CurrencyRepository.php
  20. 34
    31
      app/Services/ChartOfAccountsService.php
  21. 7
    10
      app/Services/CompanyDefaultService.php
  22. 4
    3
      app/Services/TransactionService.php
  23. 137
    136
      composer.lock
  24. 41
    1
      database/factories/Accounting/AccountFactory.php
  25. 54
    11
      database/factories/Accounting/TransactionFactory.php
  26. 10
    1
      database/factories/Banking/BankAccountFactory.php
  27. 7
    10
      database/factories/CompanyFactory.php
  28. 9
    9
      database/factories/Setting/CompanyDefaultFactory.php
  29. 2
    2
      database/factories/Setting/CurrencyFactory.php
  30. 15
    6
      database/factories/Setting/DiscountFactory.php
  31. 13
    5
      database/factories/Setting/TaxFactory.php
  32. 6
    9
      database/seeders/DatabaseSeeder.php
  33. 4
    1
      database/seeders/TestDatabaseSeeder.php
  34. 73
    73
      package-lock.json
  35. 2
    2
      resources/views/components/panel-shift-dropdown.blade.php
  36. 515
    0
      tests/Feature/Accounting/TransactionTest.php
  37. 0
    7
      tests/Feature/ExampleTest.php
  38. 16
    0
      tests/Helpers/helpers.php

+ 9
- 19
app/Collections/Accounting/JournalEntryCollection.php 查看文件

@@ -2,9 +2,9 @@
2 2
 
3 3
 namespace App\Collections\Accounting;
4 4
 
5
+use App\Enums\Accounting\JournalEntryType;
5 6
 use App\Models\Accounting\JournalEntry;
6 7
 use App\Utilities\Currency\CurrencyAccessor;
7
-use App\Utilities\Currency\CurrencyConverter;
8 8
 use App\ValueObjects\Money;
9 9
 use Illuminate\Database\Eloquent\Collection;
10 10
 
@@ -12,30 +12,20 @@ class JournalEntryCollection extends Collection
12 12
 {
13 13
     public function sumDebits(): Money
14 14
     {
15
-        $total = $this->reduce(static function ($carry, JournalEntry $item) {
16
-            if ($item->type->isDebit()) {
17
-                $amountAsInteger = CurrencyConverter::prepareForAccessor($item->amount, CurrencyAccessor::getDefaultCurrency());
18
-
19
-                return bcadd($carry, $amountAsInteger, 0);
20
-            }
21
-
22
-            return $carry;
23
-        }, 0);
15
+        $total = $this->where('type', JournalEntryType::Debit)
16
+            ->sum(static function (JournalEntry $item) {
17
+                return $item->rawValue('amount');
18
+            });
24 19
 
25 20
         return new Money($total, CurrencyAccessor::getDefaultCurrency());
26 21
     }
27 22
 
28 23
     public function sumCredits(): Money
29 24
     {
30
-        $total = $this->reduce(static function ($carry, JournalEntry $item) {
31
-            if ($item->type->isCredit()) {
32
-                $amountAsInteger = CurrencyConverter::prepareForAccessor($item->amount, CurrencyAccessor::getDefaultCurrency());
33
-
34
-                return bcadd($carry, $amountAsInteger, 0);
35
-            }
36
-
37
-            return $carry;
38
-        }, 0);
25
+        $total = $this->where('type', JournalEntryType::Credit)
26
+            ->sum(static function (JournalEntry $item) {
27
+                return $item->rawValue('amount');
28
+            });
39 29
 
40 30
         return new Money($total, CurrencyAccessor::getDefaultCurrency());
41 31
     }

+ 1
- 5
app/Concerns/CompanyOwned.php 查看文件

@@ -17,15 +17,11 @@ trait CompanyOwned
17 17
             if (empty($model->company_id)) {
18 18
                 $companyId = session('current_company_id');
19 19
 
20
-                if (! $companyId && Auth::check() && Auth::user()->currentCompany) {
20
+                if (! $companyId && Auth::check()) {
21 21
                     $companyId = Auth::user()->currentCompany->id;
22 22
                     session(['current_company_id' => $companyId]);
23 23
                 }
24 24
 
25
-                if (! $companyId) {
26
-                    $companyId = Auth::user()->currentCompany->id;
27
-                }
28
-
29 25
                 if ($companyId) {
30 26
                     $model->company_id = $companyId;
31 27
                 } else {

+ 0
- 51
app/Concerns/HandlesResourceRecordCreation.php 查看文件

@@ -1,51 +0,0 @@
1
-<?php
2
-
3
-namespace App\Concerns;
4
-
5
-use App\Models\User;
6
-use Illuminate\Database\Eloquent\Builder;
7
-use Illuminate\Database\Eloquent\Model;
8
-
9
-trait HandlesResourceRecordCreation
10
-{
11
-    protected function handleRecordCreationWithUniqueField(array $data, Model $model, User $user, ?string $uniqueField = null, ?array $evaluatedTypes = null): Model
12
-    {
13
-        // If evaluatedTypes is provided, ensure the unique field value is within the allowed types
14
-        if ($uniqueField && $evaluatedTypes && ! in_array($data[$uniqueField] ?? '', $evaluatedTypes, true)) {
15
-            $data['enabled'] = false;
16
-            $instance = $model->newInstance($data);
17
-            $instance->save();
18
-
19
-            return $instance;
20
-        }
21
-
22
-        $companyId = $user->currentCompany->id;
23
-        $shouldBeEnabled = (bool) ($data['enabled'] ?? false);
24
-
25
-        $query = $model::query()
26
-            ->where('company_id', $companyId)
27
-            ->where('enabled', true);
28
-
29
-        if ($uniqueField && array_key_exists($uniqueField, $data)) {
30
-            $query->where($uniqueField, $data[$uniqueField]);
31
-        }
32
-
33
-        $this->toggleRecords($query, $shouldBeEnabled);
34
-
35
-        $data['enabled'] = $shouldBeEnabled;
36
-        $instance = $model->newInstance($data);
37
-        $instance->save();
38
-
39
-        return $instance;
40
-    }
41
-
42
-    private function toggleRecords(Builder $query, bool &$shouldBeEnabled): void
43
-    {
44
-        if ($shouldBeEnabled) {
45
-            $existingEnabledRecord = $query->first();
46
-            $existingEnabledRecord?->update(['enabled' => false]);
47
-        } elseif ($query->doesntExist()) {
48
-            $shouldBeEnabled = true;
49
-        }
50
-    }
51
-}

+ 0
- 80
app/Concerns/HandlesResourceRecordUpdate.php 查看文件

@@ -1,80 +0,0 @@
1
-<?php
2
-
3
-namespace App\Concerns;
4
-
5
-use App\Models\User;
6
-use BackedEnum;
7
-use Illuminate\Database\Eloquent\Builder;
8
-use Illuminate\Database\Eloquent\Model;
9
-
10
-trait HandlesResourceRecordUpdate
11
-{
12
-    protected function handleRecordUpdateWithUniqueField(Model $record, array $data, User $user, ?string $uniqueField = null, ?array $evaluatedTypes = null): Model
13
-    {
14
-        if (is_array($evaluatedTypes)) {
15
-            $evaluatedTypes = $this->ensureUpdateEnumValues($evaluatedTypes);
16
-        }
17
-
18
-        if ($uniqueField && ! in_array($data[$uniqueField] ?? '', $evaluatedTypes ?? [], true)) {
19
-            $data['enabled'] = false;
20
-
21
-            return tap($record)->update($data);
22
-        }
23
-
24
-        $companyId = $user->currentCompany->id;
25
-        $oldValue = $uniqueField ? $record->{$uniqueField} : null;
26
-        $newValue = $uniqueField ? $data[$uniqueField] : null;
27
-        $enabled = (bool) ($data['enabled'] ?? false);
28
-        $wasOriginallyEnabled = (bool) $record->getAttribute('enabled');
29
-
30
-        if ($oldValue instanceof BackedEnum) {
31
-            $oldValue = $oldValue->value;
32
-        }
33
-
34
-        if ($newValue instanceof BackedEnum) {
35
-            $newValue = $newValue->value;
36
-        }
37
-
38
-        if ($uniqueField && $oldValue !== $newValue && $wasOriginallyEnabled) {
39
-            $newValue = $oldValue;
40
-            $data[$uniqueField] = $oldValue;
41
-        }
42
-
43
-        if ($enabled === true && ! $wasOriginallyEnabled) {
44
-            $this->toggleRecord($companyId, $record, $uniqueField, $newValue, true, false);
45
-        } elseif ($enabled === false && $wasOriginallyEnabled) {
46
-            $enabled = true;
47
-        }
48
-
49
-        $data['enabled'] = $enabled;
50
-
51
-        return tap($record)->update($data);
52
-    }
53
-
54
-    private function ensureUpdateEnumValues(array $evaluatedTypes): array
55
-    {
56
-        return array_map(static function ($type) {
57
-            return $type instanceof BackedEnum ? $type->value : $type;
58
-        }, $evaluatedTypes);
59
-    }
60
-
61
-    protected function toggleRecord(int $companyId, Model $record, ?string $uniqueField, $value, bool $enabled, bool $newStatus): void
62
-    {
63
-        $query = $this->buildQuery($companyId, $record, $uniqueField, $value, $enabled);
64
-
65
-        if ($newStatus && ($otherRecord = $query->first())) {
66
-            $otherRecord->update(['enabled' => true]);
67
-        } else {
68
-            $query->update(['enabled' => false]);
69
-        }
70
-    }
71
-
72
-    protected function buildQuery(int $companyId, Model $record, ?string $uniqueField, $value, bool $enabled): Builder
73
-    {
74
-        return $record::query()
75
-            ->where('company_id', $companyId)
76
-            ->where('id', '!=', $record->getKey())
77
-            ->where('enabled', $enabled)
78
-            ->when($uniqueField, static fn ($q) => $q->where($uniqueField, $value));
79
-    }
80
-}

+ 28
- 0
app/Concerns/HasDefault.php 查看文件

@@ -2,6 +2,9 @@
2 2
 
3 3
 namespace App\Concerns;
4 4
 
5
+use Illuminate\Database\Eloquent\Builder;
6
+use Illuminate\Database\Eloquent\Model;
7
+
5 8
 trait HasDefault
6 9
 {
7 10
     public function isEnabled(): bool
@@ -23,4 +26,29 @@ trait HasDefault
23 26
     {
24 27
         return translate('No');
25 28
     }
29
+
30
+    public static function bootHasDefault(): void
31
+    {
32
+        static::saving(function (Model $model) {
33
+            if ($model->isDirty(['enabled', $model->evaluatedDefault])) {
34
+                if ($model->enabled === true) {
35
+                    static::query()
36
+                        ->whereKeyNot($model->getKey())
37
+                        ->where('enabled', true)
38
+                        ->when(filled($model->evaluatedDefault), static fn (Builder $query) => $query->where($model->evaluatedDefault, $model->{$model->evaluatedDefault}))
39
+                        ->update(['enabled' => false]);
40
+                } else {
41
+                    $enabledRecordDoesNotExist = static::query()
42
+                        ->whereKeyNot($model->getKey())
43
+                        ->where('enabled', true)
44
+                        ->when(filled($model->evaluatedDefault), static fn (Builder $query) => $query->where($model->evaluatedDefault, $model->{$model->evaluatedDefault}))
45
+                        ->doesntExist();
46
+
47
+                    if ($enabledRecordDoesNotExist) {
48
+                        $model->enabled = true;
49
+                    }
50
+                }
51
+            }
52
+        });
53
+    }
26 54
 }

+ 12
- 32
app/Filament/Company/Clusters/Settings/Resources/DiscountResource.php 查看文件

@@ -50,13 +50,13 @@ class DiscountResource extends Resource
50 50
                             ->localizeLabel()
51 51
                             ->maxLength(255)
52 52
                             ->rule(static function (Forms\Get $get, Forms\Components\Component $component): Closure {
53
-                                return static function (string $attribute, $value, Closure $fail) use ($get, $component) {
54
-                                    $existingDiscount = Discount::where('company_id', auth()->user()->currentCompany->id)
55
-                                        ->where('name', $value)
53
+                                return static function (string $attribute, $value, Closure $fail) use ($component, $get) {
54
+                                    $existingDiscount = Discount::where('name', $value)
55
+                                        ->whereKeyNot($component->getRecord()?->getKey())
56 56
                                         ->where('type', $get('type'))
57 57
                                         ->first();
58 58
 
59
-                                    if ($existingDiscount && $existingDiscount->getKey() !== $component->getRecord()?->getKey()) {
59
+                                    if ($existingDiscount) {
60 60
                                         $message = translate('The :Type :record ":name" already exists.', [
61 61
                                             'Type' => $existingDiscount->type->getLabel(),
62 62
                                             'record' => strtolower(static::getModelLabel()),
@@ -90,35 +90,13 @@ class DiscountResource extends Resource
90 90
                             ->nullable(),
91 91
                         Forms\Components\DateTimePicker::make('start_date')
92 92
                             ->localizeLabel()
93
-                            ->minDate(static function ($context, ?Discount $record = null) {
94
-                                if ($context === 'create' || $record?->start_date?->isFuture()) {
95
-                                    return today()->addDay();
96
-                                }
97
-
98
-                                return $record?->start_date;
99
-                            })
100
-                            ->maxDate(static function (callable $get, ?Discount $record = null) {
101
-                                $end_date = $get('end_date') ?? $record?->end_date;
102
-
103
-                                return $end_date ?: today()->addYear();
104
-                            })
105
-                            ->format('Y-m-d H:i:s')
106
-                            ->displayFormat('F d, Y H:i')
93
+                            ->beforeOrEqual('end_date')
107 94
                             ->seconds(false)
108
-                            ->live()
109
-                            ->disabled(static fn ($context, ?Discount $record = null) => $context === 'edit' && $record?->start_date?->isPast() ?? false)
95
+                            ->disabled(static fn (string $operation, ?Discount $record = null) => $operation === 'edit' && $record?->start_date?->isPast() ?? false)
110 96
                             ->helperText(static fn (Forms\Components\DateTimePicker $component) => $component->isDisabled() ? 'Start date cannot be changed after the discount has begun.' : null),
111 97
                         Forms\Components\DateTimePicker::make('end_date')
112
-                            ->live()
113 98
                             ->localizeLabel()
114
-                            ->minDate(static function (callable $get, ?Discount $record = null) {
115
-                                $start_date = $get('start_date') ?? $record?->start_date;
116
-
117
-                                return $start_date ?: today()->addDay();
118
-                            })
119
-                            ->maxDate(today()->addYear())
120
-                            ->format('Y-m-d H:i:s')
121
-                            ->displayFormat('F d, Y H:i')
99
+                            ->afterOrEqual('start_date')
122 100
                             ->seconds(false),
123 101
                         ToggleButton::make('enabled')
124 102
                             ->localizeLabel('Default')
@@ -137,12 +115,14 @@ class DiscountResource extends Resource
137 115
                     ->weight(FontWeight::Medium)
138 116
                     ->icon(static fn (Discount $record) => $record->isEnabled() ? 'heroicon-o-lock-closed' : null)
139 117
                     ->tooltip(static function (Discount $record) {
140
-                        $tooltipMessage = translate('Default :Type :Record', [
118
+                        if ($record->isDisabled()) {
119
+                            return null;
120
+                        }
121
+
122
+                        return translate('Default :Type :Record', [
141 123
                             'Type' => $record->type->getLabel(),
142 124
                             'Record' => static::getModelLabel(),
143 125
                         ]);
144
-
145
-                        return $record->isEnabled() ? $tooltipMessage : null;
146 126
                     })
147 127
                     ->iconPosition('after')
148 128
                     ->searchable()

+ 0
- 30
app/Filament/Company/Clusters/Settings/Resources/DiscountResource/Pages/CreateDiscount.php 查看文件

@@ -2,45 +2,15 @@
2 2
 
3 3
 namespace App\Filament\Company\Clusters\Settings\Resources\DiscountResource\Pages;
4 4
 
5
-use App\Concerns\HandlesResourceRecordCreation;
6
-use App\Enums\Setting\DiscountType;
7 5
 use App\Filament\Company\Clusters\Settings\Resources\DiscountResource;
8
-use App\Models\Setting\Discount;
9 6
 use Filament\Resources\Pages\CreateRecord;
10
-use Filament\Support\Exceptions\Halt;
11
-use Illuminate\Database\Eloquent\Model;
12 7
 
13 8
 class CreateDiscount extends CreateRecord
14 9
 {
15
-    use HandlesResourceRecordCreation;
16
-
17 10
     protected static string $resource = DiscountResource::class;
18 11
 
19 12
     protected function getRedirectUrl(): string
20 13
     {
21 14
         return $this->getResource()::getUrl('index');
22 15
     }
23
-
24
-    protected function mutateFormDataBeforeCreate(array $data): array
25
-    {
26
-        $data['enabled'] = (bool) $data['enabled'];
27
-
28
-        return $data;
29
-    }
30
-
31
-    /**
32
-     * @throws Halt
33
-     */
34
-    protected function handleRecordCreation(array $data): Model
35
-    {
36
-        $user = auth()->user();
37
-
38
-        if (! $user) {
39
-            throw new Halt('No authenticated user found');
40
-        }
41
-
42
-        $evaluatedTypes = [DiscountType::Sales, DiscountType::Purchase];
43
-
44
-        return $this->handleRecordCreationWithUniqueField($data, new Discount, $user, 'type', $evaluatedTypes);
45
-    }
46 16
 }

+ 0
- 29
app/Filament/Company/Clusters/Settings/Resources/DiscountResource/Pages/EditDiscount.php 查看文件

@@ -2,18 +2,12 @@
2 2
 
3 3
 namespace App\Filament\Company\Clusters\Settings\Resources\DiscountResource\Pages;
4 4
 
5
-use App\Concerns\HandlesResourceRecordUpdate;
6
-use App\Enums\Setting\DiscountType;
7 5
 use App\Filament\Company\Clusters\Settings\Resources\DiscountResource;
8 6
 use Filament\Actions;
9 7
 use Filament\Resources\Pages\EditRecord;
10
-use Filament\Support\Exceptions\Halt;
11
-use Illuminate\Database\Eloquent\Model;
12 8
 
13 9
 class EditDiscount extends EditRecord
14 10
 {
15
-    use HandlesResourceRecordUpdate;
16
-
17 11
     protected static string $resource = DiscountResource::class;
18 12
 
19 13
     protected function getHeaderActions(): array
@@ -27,27 +21,4 @@ class EditDiscount extends EditRecord
27 21
     {
28 22
         return $this->getResource()::getUrl('index');
29 23
     }
30
-
31
-    protected function mutateFormDataBeforeSave(array $data): array
32
-    {
33
-        $data['enabled'] = (bool) $data['enabled'];
34
-
35
-        return $data;
36
-    }
37
-
38
-    /**
39
-     * @throws Halt
40
-     */
41
-    protected function handleRecordUpdate(Model $record, array $data): Model
42
-    {
43
-        $user = auth()->user();
44
-
45
-        if (! $user) {
46
-            throw new Halt('No authenticated user found');
47
-        }
48
-
49
-        $evaluatedTypes = [DiscountType::Sales, DiscountType::Purchase];
50
-
51
-        return $this->handleRecordUpdateWithUniqueField($record, $data, $user, 'type', $evaluatedTypes);
52
-    }
53 24
 }

+ 9
- 7
app/Filament/Company/Clusters/Settings/Resources/TaxResource.php 查看文件

@@ -47,13 +47,13 @@ class TaxResource extends Resource
47 47
                             ->localizeLabel()
48 48
                             ->maxLength(255)
49 49
                             ->rule(static function (Forms\Get $get, Forms\Components\Component $component): Closure {
50
-                                return static function (string $attribute, $value, Closure $fail) use ($get, $component) {
51
-                                    $existingTax = Tax::where('company_id', auth()->user()->currentCompany->id)
52
-                                        ->where('name', $value)
50
+                                return static function (string $attribute, $value, Closure $fail) use ($component, $get) {
51
+                                    $existingTax = Tax::where('name', $value)
52
+                                        ->whereKeyNot($component->getRecord()?->getKey())
53 53
                                         ->where('type', $get('type'))
54 54
                                         ->first();
55 55
 
56
-                                    if ($existingTax && $existingTax->getKey() !== $component->getRecord()?->getKey()) {
56
+                                    if ($existingTax) {
57 57
                                         $message = translate('The :Type :record ":name" already exists.', [
58 58
                                             'Type' => $existingTax->type->getLabel(),
59 59
                                             'record' => strtolower(static::getModelLabel()),
@@ -100,12 +100,14 @@ class TaxResource extends Resource
100 100
                     ->weight(FontWeight::Medium)
101 101
                     ->icon(static fn (Tax $record) => $record->isEnabled() ? 'heroicon-o-lock-closed' : null)
102 102
                     ->tooltip(static function (Tax $record) {
103
-                        $tooltipMessage = translate('Default :Type :Record', [
103
+                        if ($record->isDisabled()) {
104
+                            return null;
105
+                        }
106
+
107
+                        return translate('Default :Type :Record', [
104 108
                             'Type' => $record->type->getLabel(),
105 109
                             'Record' => static::getModelLabel(),
106 110
                         ]);
107
-
108
-                        return $record->isEnabled() ? $tooltipMessage : null;
109 111
                     })
110 112
                     ->iconPosition('after')
111 113
                     ->searchable()

+ 0
- 30
app/Filament/Company/Clusters/Settings/Resources/TaxResource/Pages/CreateTax.php 查看文件

@@ -2,45 +2,15 @@
2 2
 
3 3
 namespace App\Filament\Company\Clusters\Settings\Resources\TaxResource\Pages;
4 4
 
5
-use App\Concerns\HandlesResourceRecordCreation;
6
-use App\Enums\Setting\TaxType;
7 5
 use App\Filament\Company\Clusters\Settings\Resources\TaxResource;
8
-use App\Models\Setting\Tax;
9 6
 use Filament\Resources\Pages\CreateRecord;
10
-use Filament\Support\Exceptions\Halt;
11
-use Illuminate\Database\Eloquent\Model;
12 7
 
13 8
 class CreateTax extends CreateRecord
14 9
 {
15
-    use HandlesResourceRecordCreation;
16
-
17 10
     protected static string $resource = TaxResource::class;
18 11
 
19 12
     protected function getRedirectUrl(): string
20 13
     {
21 14
         return $this->getResource()::getUrl('index');
22 15
     }
23
-
24
-    protected function mutateFormDataBeforeCreate(array $data): array
25
-    {
26
-        $data['enabled'] = (bool) $data['enabled'];
27
-
28
-        return $data;
29
-    }
30
-
31
-    /**
32
-     * @throws Halt
33
-     */
34
-    protected function handleRecordCreation(array $data): Model
35
-    {
36
-        $user = auth()->user();
37
-
38
-        if (! $user) {
39
-            throw new Halt('No authenticated user found');
40
-        }
41
-
42
-        $evaluatedTypes = [TaxType::Sales, TaxType::Purchase];
43
-
44
-        return $this->handleRecordCreationWithUniqueField($data, new Tax, $user, 'type', $evaluatedTypes);
45
-    }
46 16
 }

+ 0
- 29
app/Filament/Company/Clusters/Settings/Resources/TaxResource/Pages/EditTax.php 查看文件

@@ -2,18 +2,12 @@
2 2
 
3 3
 namespace App\Filament\Company\Clusters\Settings\Resources\TaxResource\Pages;
4 4
 
5
-use App\Concerns\HandlesResourceRecordUpdate;
6
-use App\Enums\Setting\TaxType;
7 5
 use App\Filament\Company\Clusters\Settings\Resources\TaxResource;
8 6
 use Filament\Actions;
9 7
 use Filament\Resources\Pages\EditRecord;
10
-use Filament\Support\Exceptions\Halt;
11
-use Illuminate\Database\Eloquent\Model;
12 8
 
13 9
 class EditTax extends EditRecord
14 10
 {
15
-    use HandlesResourceRecordUpdate;
16
-
17 11
     protected static string $resource = TaxResource::class;
18 12
 
19 13
     protected function getHeaderActions(): array
@@ -27,27 +21,4 @@ class EditTax extends EditRecord
27 21
     {
28 22
         return $this->getResource()::getUrl('index');
29 23
     }
30
-
31
-    protected function mutateFormDataBeforeSave(array $data): array
32
-    {
33
-        $data['enabled'] = (bool) $data['enabled'];
34
-
35
-        return $data;
36
-    }
37
-
38
-    /**
39
-     * @throws Halt
40
-     */
41
-    protected function handleRecordUpdate(Model $record, array $data): Model
42
-    {
43
-        $user = auth()->user();
44
-
45
-        if (! $user) {
46
-            throw new Halt('No authenticated user found');
47
-        }
48
-
49
-        $evaluatedTypes = [TaxType::Sales, TaxType::Purchase];
50
-
51
-        return $this->handleRecordUpdateWithUniqueField($record, $data, $user, 'type', $evaluatedTypes);
52
-    }
53 24
 }

+ 1
- 1
app/Filament/Company/Pages/Accounting/Transactions.php 查看文件

@@ -497,7 +497,7 @@ class Transactions extends Page implements HasTable
497 497
         ];
498 498
     }
499 499
 
500
-    protected static function getUncategorizedAccountByType(TransactionType $type): ?Account
500
+    public static function getUncategorizedAccountByType(TransactionType $type): ?Account
501 501
     {
502 502
         [$category, $accountName] = match ($type) {
503 503
             TransactionType::Deposit => [AccountCategory::Revenue, 'Uncategorized Income'],

+ 3
- 1
app/Filament/Company/Pages/CreateCompany.php 查看文件

@@ -21,6 +21,8 @@ use Wallo\FilamentCompanies\Pages\Company\CreateCompany as FilamentCreateCompany
21 21
 
22 22
 class CreateCompany extends FilamentCreateCompany
23 23
 {
24
+    protected bool $hasTopbar = false;
25
+
24 26
     public function form(Form $form): Form
25 27
     {
26 28
         return $form
@@ -85,7 +87,7 @@ class CreateCompany extends FilamentCreateCompany
85 87
 
86 88
             $user?->switchCompany($company);
87 89
 
88
-            $companyDefaultService = app()->make(CompanyDefaultService::class);
90
+            $companyDefaultService = app(CompanyDefaultService::class);
89 91
             $user = $company->owner ?? $user;
90 92
             $companyDefaultService->createCompanyDefaults($company, $user, $data['currencies']['code'], $data['profile']['country'], $data['locale']['language']);
91 93
 

+ 0
- 21
app/Filament/Company/Resources/Banking/AccountResource/Pages/CreateAccount.php 查看文件

@@ -2,18 +2,11 @@
2 2
 
3 3
 namespace App\Filament\Company\Resources\Banking\AccountResource\Pages;
4 4
 
5
-use App\Concerns\HandlesResourceRecordCreation;
6 5
 use App\Filament\Company\Resources\Banking\AccountResource;
7
-use App\Models\Banking\BankAccount;
8 6
 use Filament\Resources\Pages\CreateRecord;
9
-use Filament\Support\Exceptions\Halt;
10
-use Illuminate\Database\Eloquent\Model;
11
-use Illuminate\Support\Facades\Auth;
12 7
 
13 8
 class CreateAccount extends CreateRecord
14 9
 {
15
-    use HandlesResourceRecordCreation;
16
-
17 10
     protected static string $resource = AccountResource::class;
18 11
 
19 12
     protected function getRedirectUrl(): string
@@ -27,18 +20,4 @@ class CreateAccount extends CreateRecord
27 20
 
28 21
         return $data;
29 22
     }
30
-
31
-    /**
32
-     * @throws Halt
33
-     */
34
-    protected function handleRecordCreation(array $data): Model
35
-    {
36
-        $user = Auth::user();
37
-
38
-        if (! $user) {
39
-            throw new Halt('No authenticated user found');
40
-        }
41
-
42
-        return $this->handleRecordCreationWithUniqueField($data, new BankAccount, $user);
43
-    }
44 23
 }

+ 0
- 20
app/Filament/Company/Resources/Banking/AccountResource/Pages/EditAccount.php 查看文件

@@ -2,17 +2,11 @@
2 2
 
3 3
 namespace App\Filament\Company\Resources\Banking\AccountResource\Pages;
4 4
 
5
-use App\Concerns\HandlesResourceRecordUpdate;
6 5
 use App\Filament\Company\Resources\Banking\AccountResource;
7 6
 use Filament\Resources\Pages\EditRecord;
8
-use Filament\Support\Exceptions\Halt;
9
-use Illuminate\Database\Eloquent\Model;
10
-use Illuminate\Support\Facades\Auth;
11 7
 
12 8
 class EditAccount extends EditRecord
13 9
 {
14
-    use HandlesResourceRecordUpdate;
15
-
16 10
     protected static string $resource = AccountResource::class;
17 11
 
18 12
     protected function getHeaderActions(): array
@@ -33,18 +27,4 @@ class EditAccount extends EditRecord
33 27
 
34 28
         return $data;
35 29
     }
36
-
37
-    /**
38
-     * @throws Halt
39
-     */
40
-    protected function handleRecordUpdate(Model $record, array $data): Model
41
-    {
42
-        $user = Auth::user();
43
-
44
-        if (! $user) {
45
-            throw new Halt('No authenticated user found');
46
-        }
47
-
48
-        return $this->handleRecordUpdateWithUniqueField($record, $data, $user);
49
-    }
50 30
 }

+ 2
- 0
app/Models/Setting/Discount.php 查看文件

@@ -51,6 +51,8 @@ class Discount extends Model
51 51
         'enabled' => 'boolean',
52 52
     ];
53 53
 
54
+    protected ?string $evaluatedDefault = 'type';
55
+
54 56
     public function defaultSalesDiscount(): HasOne
55 57
     {
56 58
         return $this->hasOne(CompanyDefault::class, 'sales_discount_id');

+ 2
- 0
app/Models/Setting/Tax.php 查看文件

@@ -47,6 +47,8 @@ class Tax extends Model
47 47
         'enabled' => 'boolean',
48 48
     ];
49 49
 
50
+    protected ?string $evaluatedDefault = 'type';
51
+
50 52
     public function defaultSalesTax(): HasOne
51 53
     {
52 54
         return $this->hasOne(CompanyDefault::class, 'sales_tax_id');

+ 11
- 3
app/Providers/MacroServiceProvider.php 查看文件

@@ -171,16 +171,24 @@ class MacroServiceProvider extends ServiceProvider
171 171
 
172 172
         Money::macro('swapAmountFor', function ($newCurrency) {
173 173
             $oldCurrency = $this->currency->getCurrency();
174
-            $balanceInMajorUnits = $this->getAmount();
174
+            $balanceInSubunits = $this->getAmount();
175
+
176
+            $oldCurrencySubunit = currency($oldCurrency)->getSubunit();
177
+            $newCurrencySubunit = currency($newCurrency)->getSubunit();
178
+
179
+            $balanceInMajorUnits = $balanceInSubunits / $oldCurrencySubunit;
175 180
 
176 181
             $oldRate = currency($oldCurrency)->getRate();
177 182
             $newRate = currency($newCurrency)->getRate();
178 183
 
179 184
             $ratio = $newRate / $oldRate;
185
+            $convertedBalanceInMajorUnits = $balanceInMajorUnits * $ratio;
186
+
187
+            $roundedConvertedBalanceInMajorUnits = round($convertedBalanceInMajorUnits, currency($newCurrency)->getPrecision());
180 188
 
181
-            $convertedBalance = bcmul($balanceInMajorUnits, $ratio, 2);
189
+            $convertedBalanceInSubunits = $roundedConvertedBalanceInMajorUnits * $newCurrencySubunit;
182 190
 
183
-            return (int) round($convertedBalance);
191
+            return (int) round($convertedBalanceInSubunits);
184 192
         });
185 193
 
186 194
         Money::macro('formatWithCode', function (bool $codeBefore = false) {

+ 0
- 42
app/Repositories/Setting/CurrencyRepository.php 查看文件

@@ -1,42 +0,0 @@
1
-<?php
2
-
3
-namespace App\Repositories\Setting;
4
-
5
-use App\Models\Company;
6
-use App\Models\Setting\Currency;
7
-
8
-class CurrencyRepository
9
-{
10
-    public function ensureCurrencyExists(Company $company, string $currencyCode): Currency
11
-    {
12
-        $hasDefaultCurrency = $this->hasDefaultCurrency($company);
13
-
14
-        $currency = currency($currencyCode);
15
-
16
-        return $company->currencies()
17
-            ->firstOrCreate([
18
-                'code' => $currencyCode,
19
-            ], [
20
-                'name' => $currency->getName(),
21
-                'rate' => $currency->getRate(),
22
-                'precision' => $currency->getPrecision(),
23
-                'symbol' => $currency->getSymbol(),
24
-                'symbol_first' => $currency->isSymbolFirst(),
25
-                'decimal_mark' => $currency->getDecimalMark(),
26
-                'thousands_separator' => $currency->getThousandsSeparator(),
27
-                'enabled' => ! $hasDefaultCurrency,
28
-            ]);
29
-    }
30
-
31
-    public function getDefaultCurrency(Company $company): ?Currency
32
-    {
33
-        return $company->currencies()
34
-            ->where('enabled', true)
35
-            ->first();
36
-    }
37
-
38
-    public function hasDefaultCurrency(Company $company): bool
39
-    {
40
-        return $this->getDefaultCurrency($company) !== null;
41
-    }
42
-}

+ 34
- 31
app/Services/ChartOfAccountsService.php 查看文件

@@ -4,11 +4,11 @@ namespace App\Services;
4 4
 
5 5
 use App\Enums\Accounting\AccountType;
6 6
 use App\Enums\Banking\BankAccountType;
7
-use App\Models\Accounting\Account;
8 7
 use App\Models\Accounting\AccountSubtype;
9 8
 use App\Models\Banking\BankAccount;
10 9
 use App\Models\Company;
11 10
 use App\Utilities\Currency\CurrencyAccessor;
11
+use Exception;
12 12
 
13 13
 class ChartOfAccountsService
14 14
 {
@@ -18,16 +18,27 @@ class ChartOfAccountsService
18 18
 
19 19
         foreach ($chartOfAccounts as $type => $subtypes) {
20 20
             foreach ($subtypes as $subtypeName => $subtypeConfig) {
21
-                $subtype = AccountSubtype::create([
22
-                    'company_id' => $company->id,
23
-                    'multi_currency' => $subtypeConfig['multi_currency'] ?? false,
24
-                    'category' => AccountType::from($type)->getCategory()->value,
25
-                    'type' => $type,
26
-                    'name' => $subtypeName,
27
-                    'description' => $subtypeConfig['description'] ?? 'No description available.',
28
-                ]);
21
+                $subtype = $company->accountSubtypes()
22
+                    ->createQuietly([
23
+                        'multi_currency' => $subtypeConfig['multi_currency'] ?? false,
24
+                        'category' => AccountType::from($type)->getCategory()->value,
25
+                        'type' => $type,
26
+                        'name' => $subtypeName,
27
+                        'description' => $subtypeConfig['description'] ?? 'No description available.',
28
+                    ]);
29
+
30
+                try {
31
+                    $this->createDefaultAccounts($company, $subtype, $subtypeConfig);
32
+                } catch (Exception $e) {
33
+                    // Log the error
34
+                    logger()->alert('Failed to create a company with its defaults, blocking critical business functionality.', [
35
+                        'error' => $e->getMessage(),
36
+                        'userId' => $company->owner->id,
37
+                        'companyId' => $company->id,
38
+                    ]);
29 39
 
30
-                $this->createDefaultAccounts($company, $subtype, $subtypeConfig);
40
+                    throw $e;
41
+                }
31 42
             }
32 43
         }
33 44
     }
@@ -37,6 +48,12 @@ class ChartOfAccountsService
37 48
         if (isset($subtypeConfig['accounts']) && is_array($subtypeConfig['accounts'])) {
38 49
             $baseCode = $subtypeConfig['base_code'];
39 50
 
51
+            $defaultCurrencyCode = CurrencyAccessor::getDefaultCurrency();
52
+
53
+            if (empty($defaultCurrencyCode)) {
54
+                throw new Exception('No default currency available for creating accounts.');
55
+            }
56
+
40 57
             foreach ($subtypeConfig['accounts'] as $accountName => $accountDetails) {
41 58
                 $bankAccount = null;
42 59
 
@@ -44,46 +61,32 @@ class ChartOfAccountsService
44 61
                     $bankAccount = $this->createBankAccountForMultiCurrency($company, $subtypeConfig['bank_account_type']);
45 62
                 }
46 63
 
47
-                $account = Account::create([
48
-                    'company_id' => $company->id,
64
+                $company->accounts()->createQuietly([
65
+                    'bank_account_id' => $bankAccount?->id,
49 66
                     'subtype_id' => $subtype->id,
50 67
                     'category' => $subtype->type->getCategory()->value,
51 68
                     'type' => $subtype->type->value,
52 69
                     'code' => $baseCode++,
53 70
                     'name' => $accountName,
54
-                    'currency_code' => CurrencyAccessor::getDefaultCurrency(),
71
+                    'currency_code' => $defaultCurrencyCode,
55 72
                     'description' => $accountDetails['description'] ?? 'No description available.',
56 73
                     'default' => true,
57 74
                     'created_by' => $company->owner->id,
58 75
                     'updated_by' => $company->owner->id,
59 76
                 ]);
60
-
61
-                if ($bankAccount) {
62
-                    $account->bankAccount()->associate($bankAccount);
63
-                }
64
-
65
-                $account->save();
66 77
             }
67 78
         }
68 79
     }
69 80
 
70 81
     private function createBankAccountForMultiCurrency(Company $company, string $bankAccountType): BankAccount
71 82
     {
72
-        $bankAccountType = BankAccountType::from($bankAccountType) ?? BankAccountType::Other;
83
+        $noDefaultBankAccount = $company->bankAccounts()->where('enabled', true)->doesntExist();
73 84
 
74
-        return BankAccount::create([
75
-            'company_id' => $company->id,
76
-            'institution_id' => null,
77
-            'type' => $bankAccountType,
78
-            'number' => null,
79
-            'enabled' => BankAccount::where('company_id', $company->id)->where('enabled', true)->doesntExist(),
85
+        return $company->bankAccounts()->createQuietly([
86
+            'type' => BankAccountType::from($bankAccountType) ?? BankAccountType::Other,
87
+            'enabled' => $noDefaultBankAccount,
80 88
             'created_by' => $company->owner->id,
81 89
             'updated_by' => $company->owner->id,
82 90
         ]);
83 91
     }
84
-
85
-    public function getDefaultBankAccount(Company $company): ?BankAccount
86
-    {
87
-        return $company->bankAccounts()->where('enabled', true)->first();
88
-    }
89 92
 }

+ 7
- 10
app/Services/CompanyDefaultService.php 查看文件

@@ -13,21 +13,18 @@ class CompanyDefaultService
13 13
     {
14 14
         DB::transaction(function () use ($user, $company, $currencyCode, $countryCode, $language) {
15 15
             // Create the company defaults
16
-            $companyDefaultFactory = CompanyDefault::factory()->withDefault($user, $company, $currencyCode, $countryCode, $language);
17
-            $companyDefaults = $companyDefaultFactory->make()->toArray();
18
-
19
-            $companyDefault = CompanyDefault::create($companyDefaults);
16
+            $companyDefaultInstance = CompanyDefault::factory()->withDefault($user, $company, $currencyCode, $countryCode, $language);
20 17
 
21 18
             // Create Chart of Accounts
22
-            $chartOfAccountsService = app()->make(ChartOfAccountsService::class);
19
+            $chartOfAccountsService = app(ChartOfAccountsService::class);
23 20
             $chartOfAccountsService->createChartOfAccounts($company);
24 21
 
25 22
             // Get the default bank account and update the company default record
26
-            $defaultBankAccount = $chartOfAccountsService->getDefaultBankAccount($company);
23
+            $defaultBankAccount = $company->bankAccounts()->where('enabled', true)->firstOrFail();
27 24
 
28
-            $companyDefault->update([
29
-                'bank_account_id' => $defaultBankAccount?->id,
30
-            ]);
31
-        }, 5);
25
+            $companyDefaultInstance->state([
26
+                'bank_account_id' => $defaultBankAccount->id,
27
+            ])->createQuietly();
28
+        });
32 29
     }
33 30
 }

+ 4
- 3
app/Services/TransactionService.php 查看文件

@@ -229,6 +229,8 @@ class TransactionService
229 229
                 'type' => JournalEntryType::Debit,
230 230
                 'amount' => $convertedTransactionAmount,
231 231
                 'description' => $transaction->description,
232
+                'created_by' => $transaction->created_by,
233
+                'updated_by' => $transaction->updated_by,
232 234
             ]);
233 235
 
234 236
             $creditAccount->journalEntries()->create([
@@ -237,6 +239,8 @@ class TransactionService
237 239
                 'type' => JournalEntryType::Credit,
238 240
                 'amount' => $convertedTransactionAmount,
239 241
                 'description' => $transaction->description,
242
+                'created_by' => $transaction->created_by,
243
+                'updated_by' => $transaction->updated_by,
240 244
             ]);
241 245
         });
242 246
     }
@@ -245,12 +249,9 @@ class TransactionService
245 249
     {
246 250
         $defaultCurrency = CurrencyAccessor::getDefaultCurrency();
247 251
         $bankAccountCurrency = $transaction->bankAccount->account->currency_code;
248
-        $chartAccountCurrency = $transaction->account->currency_code;
249 252
 
250 253
         if ($bankAccountCurrency !== $defaultCurrency) {
251 254
             return $this->convertToDefaultCurrency($transaction->amount, $bankAccountCurrency, $defaultCurrency);
252
-        } elseif ($chartAccountCurrency !== $defaultCurrency) {
253
-            return $this->convertToDefaultCurrency($transaction->amount, $chartAccountCurrency, $defaultCurrency);
254 255
         }
255 256
 
256 257
         return $transaction->amount;

+ 137
- 136
composer.lock 查看文件

@@ -497,16 +497,16 @@
497 497
         },
498 498
         {
499 499
             "name": "aws/aws-sdk-php",
500
-            "version": "3.322.8",
500
+            "version": "3.323.3",
501 501
             "source": {
502 502
                 "type": "git",
503 503
                 "url": "https://github.com/aws/aws-sdk-php.git",
504
-                "reference": "fb5099160e49b676277ae787ff721628e5e4dd5a"
504
+                "reference": "9dec2a6453bdb32b3abeb475fc63b46ba1cbd996"
505 505
             },
506 506
             "dist": {
507 507
                 "type": "zip",
508
-                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/fb5099160e49b676277ae787ff721628e5e4dd5a",
509
-                "reference": "fb5099160e49b676277ae787ff721628e5e4dd5a",
508
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/9dec2a6453bdb32b3abeb475fc63b46ba1cbd996",
509
+                "reference": "9dec2a6453bdb32b3abeb475fc63b46ba1cbd996",
510 510
                 "shasum": ""
511 511
             },
512 512
             "require": {
@@ -589,9 +589,9 @@
589 589
             "support": {
590 590
                 "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
591 591
                 "issues": "https://github.com/aws/aws-sdk-php/issues",
592
-                "source": "https://github.com/aws/aws-sdk-php/tree/3.322.8"
592
+                "source": "https://github.com/aws/aws-sdk-php/tree/3.323.3"
593 593
             },
594
-            "time": "2024-09-30T19:09:25+00:00"
594
+            "time": "2024-10-08T18:05:47+00:00"
595 595
         },
596 596
         {
597 597
             "name": "aws/aws-sdk-php-laravel",
@@ -1660,16 +1660,16 @@
1660 1660
         },
1661 1661
         {
1662 1662
             "name": "filament/actions",
1663
-            "version": "v3.2.115",
1663
+            "version": "v3.2.116",
1664 1664
             "source": {
1665 1665
                 "type": "git",
1666 1666
                 "url": "https://github.com/filamentphp/actions.git",
1667
-                "reference": "38c6eb00c7e3265907b37482c2dfd411c6f910c9"
1667
+                "reference": "79dce62359b61b755a5e3d8faa0e047a1f92ad9a"
1668 1668
             },
1669 1669
             "dist": {
1670 1670
                 "type": "zip",
1671
-                "url": "https://api.github.com/repos/filamentphp/actions/zipball/38c6eb00c7e3265907b37482c2dfd411c6f910c9",
1672
-                "reference": "38c6eb00c7e3265907b37482c2dfd411c6f910c9",
1671
+                "url": "https://api.github.com/repos/filamentphp/actions/zipball/79dce62359b61b755a5e3d8faa0e047a1f92ad9a",
1672
+                "reference": "79dce62359b61b755a5e3d8faa0e047a1f92ad9a",
1673 1673
                 "shasum": ""
1674 1674
             },
1675 1675
             "require": {
@@ -1709,20 +1709,20 @@
1709 1709
                 "issues": "https://github.com/filamentphp/filament/issues",
1710 1710
                 "source": "https://github.com/filamentphp/filament"
1711 1711
             },
1712
-            "time": "2024-09-27T13:16:08+00:00"
1712
+            "time": "2024-10-08T14:24:13+00:00"
1713 1713
         },
1714 1714
         {
1715 1715
             "name": "filament/filament",
1716
-            "version": "v3.2.115",
1716
+            "version": "v3.2.116",
1717 1717
             "source": {
1718 1718
                 "type": "git",
1719 1719
                 "url": "https://github.com/filamentphp/panels.git",
1720
-                "reference": "8d0f0e7101c14fe2f00490172452767f16b39f02"
1720
+                "reference": "84f839b4b42549c0d4bd231648da17561ada70c2"
1721 1721
             },
1722 1722
             "dist": {
1723 1723
                 "type": "zip",
1724
-                "url": "https://api.github.com/repos/filamentphp/panels/zipball/8d0f0e7101c14fe2f00490172452767f16b39f02",
1725
-                "reference": "8d0f0e7101c14fe2f00490172452767f16b39f02",
1724
+                "url": "https://api.github.com/repos/filamentphp/panels/zipball/84f839b4b42549c0d4bd231648da17561ada70c2",
1725
+                "reference": "84f839b4b42549c0d4bd231648da17561ada70c2",
1726 1726
                 "shasum": ""
1727 1727
             },
1728 1728
             "require": {
@@ -1774,20 +1774,20 @@
1774 1774
                 "issues": "https://github.com/filamentphp/filament/issues",
1775 1775
                 "source": "https://github.com/filamentphp/filament"
1776 1776
             },
1777
-            "time": "2024-09-27T13:16:11+00:00"
1777
+            "time": "2024-10-08T14:24:12+00:00"
1778 1778
         },
1779 1779
         {
1780 1780
             "name": "filament/forms",
1781
-            "version": "v3.2.115",
1781
+            "version": "v3.2.116",
1782 1782
             "source": {
1783 1783
                 "type": "git",
1784 1784
                 "url": "https://github.com/filamentphp/forms.git",
1785
-                "reference": "ffa33043ea0ee67a4eed58535687f87311e4256b"
1785
+                "reference": "94f0c7c66d766efc0f1f5c80e790e2391c7c09d7"
1786 1786
             },
1787 1787
             "dist": {
1788 1788
                 "type": "zip",
1789
-                "url": "https://api.github.com/repos/filamentphp/forms/zipball/ffa33043ea0ee67a4eed58535687f87311e4256b",
1790
-                "reference": "ffa33043ea0ee67a4eed58535687f87311e4256b",
1789
+                "url": "https://api.github.com/repos/filamentphp/forms/zipball/94f0c7c66d766efc0f1f5c80e790e2391c7c09d7",
1790
+                "reference": "94f0c7c66d766efc0f1f5c80e790e2391c7c09d7",
1791 1791
                 "shasum": ""
1792 1792
             },
1793 1793
             "require": {
@@ -1830,20 +1830,20 @@
1830 1830
                 "issues": "https://github.com/filamentphp/filament/issues",
1831 1831
                 "source": "https://github.com/filamentphp/filament"
1832 1832
             },
1833
-            "time": "2024-09-27T13:16:04+00:00"
1833
+            "time": "2024-10-08T14:24:11+00:00"
1834 1834
         },
1835 1835
         {
1836 1836
             "name": "filament/infolists",
1837
-            "version": "v3.2.115",
1837
+            "version": "v3.2.116",
1838 1838
             "source": {
1839 1839
                 "type": "git",
1840 1840
                 "url": "https://github.com/filamentphp/infolists.git",
1841
-                "reference": "d4d3030644e3617aed252a5df3c385145ada0ec6"
1841
+                "reference": "fc5f01c094fe25ef906f3e1b88d3d8883a73d6be"
1842 1842
             },
1843 1843
             "dist": {
1844 1844
                 "type": "zip",
1845
-                "url": "https://api.github.com/repos/filamentphp/infolists/zipball/d4d3030644e3617aed252a5df3c385145ada0ec6",
1846
-                "reference": "d4d3030644e3617aed252a5df3c385145ada0ec6",
1845
+                "url": "https://api.github.com/repos/filamentphp/infolists/zipball/fc5f01c094fe25ef906f3e1b88d3d8883a73d6be",
1846
+                "reference": "fc5f01c094fe25ef906f3e1b88d3d8883a73d6be",
1847 1847
                 "shasum": ""
1848 1848
             },
1849 1849
             "require": {
@@ -1881,20 +1881,20 @@
1881 1881
                 "issues": "https://github.com/filamentphp/filament/issues",
1882 1882
                 "source": "https://github.com/filamentphp/filament"
1883 1883
             },
1884
-            "time": "2024-09-27T13:16:10+00:00"
1884
+            "time": "2024-10-08T14:24:09+00:00"
1885 1885
         },
1886 1886
         {
1887 1887
             "name": "filament/notifications",
1888
-            "version": "v3.2.115",
1888
+            "version": "v3.2.116",
1889 1889
             "source": {
1890 1890
                 "type": "git",
1891 1891
                 "url": "https://github.com/filamentphp/notifications.git",
1892
-                "reference": "0272612e1d54e0520f8717b24c71b9b70f198c8f"
1892
+                "reference": "a5f684b690354630210fc9a90bd06da9b1f6ae82"
1893 1893
             },
1894 1894
             "dist": {
1895 1895
                 "type": "zip",
1896
-                "url": "https://api.github.com/repos/filamentphp/notifications/zipball/0272612e1d54e0520f8717b24c71b9b70f198c8f",
1897
-                "reference": "0272612e1d54e0520f8717b24c71b9b70f198c8f",
1896
+                "url": "https://api.github.com/repos/filamentphp/notifications/zipball/a5f684b690354630210fc9a90bd06da9b1f6ae82",
1897
+                "reference": "a5f684b690354630210fc9a90bd06da9b1f6ae82",
1898 1898
                 "shasum": ""
1899 1899
             },
1900 1900
             "require": {
@@ -1933,20 +1933,20 @@
1933 1933
                 "issues": "https://github.com/filamentphp/filament/issues",
1934 1934
                 "source": "https://github.com/filamentphp/filament"
1935 1935
             },
1936
-            "time": "2024-09-27T13:16:07+00:00"
1936
+            "time": "2024-10-08T14:24:11+00:00"
1937 1937
         },
1938 1938
         {
1939 1939
             "name": "filament/support",
1940
-            "version": "v3.2.115",
1940
+            "version": "v3.2.116",
1941 1941
             "source": {
1942 1942
                 "type": "git",
1943 1943
                 "url": "https://github.com/filamentphp/support.git",
1944
-                "reference": "6dba51efd6f2a32db21bc8684cd663915ab0e4d7"
1944
+                "reference": "31fcff80b873b4decdba10d5f7010310e12c8e94"
1945 1945
             },
1946 1946
             "dist": {
1947 1947
                 "type": "zip",
1948
-                "url": "https://api.github.com/repos/filamentphp/support/zipball/6dba51efd6f2a32db21bc8684cd663915ab0e4d7",
1949
-                "reference": "6dba51efd6f2a32db21bc8684cd663915ab0e4d7",
1948
+                "url": "https://api.github.com/repos/filamentphp/support/zipball/31fcff80b873b4decdba10d5f7010310e12c8e94",
1949
+                "reference": "31fcff80b873b4decdba10d5f7010310e12c8e94",
1950 1950
                 "shasum": ""
1951 1951
             },
1952 1952
             "require": {
@@ -1956,7 +1956,7 @@
1956 1956
                 "illuminate/contracts": "^10.45|^11.0",
1957 1957
                 "illuminate/support": "^10.45|^11.0",
1958 1958
                 "illuminate/view": "^10.45|^11.0",
1959
-                "kirschbaum-development/eloquent-power-joins": "^3.0",
1959
+                "kirschbaum-development/eloquent-power-joins": "^3.0|^4.0",
1960 1960
                 "livewire/livewire": "^3.4.10 <= 3.5.6",
1961 1961
                 "php": "^8.1",
1962 1962
                 "ryangjchandler/blade-capture-directive": "^0.2|^0.3|^1.0",
@@ -1992,20 +1992,20 @@
1992 1992
                 "issues": "https://github.com/filamentphp/filament/issues",
1993 1993
                 "source": "https://github.com/filamentphp/filament"
1994 1994
             },
1995
-            "time": "2024-09-27T13:16:20+00:00"
1995
+            "time": "2024-10-08T14:24:29+00:00"
1996 1996
         },
1997 1997
         {
1998 1998
             "name": "filament/tables",
1999
-            "version": "v3.2.115",
1999
+            "version": "v3.2.116",
2000 2000
             "source": {
2001 2001
                 "type": "git",
2002 2002
                 "url": "https://github.com/filamentphp/tables.git",
2003
-                "reference": "07226fcd080f0f547aac31cf5117bfab192ea770"
2003
+                "reference": "152bf46a8f2c46f047835771a67085c2866b039b"
2004 2004
             },
2005 2005
             "dist": {
2006 2006
                 "type": "zip",
2007
-                "url": "https://api.github.com/repos/filamentphp/tables/zipball/07226fcd080f0f547aac31cf5117bfab192ea770",
2008
-                "reference": "07226fcd080f0f547aac31cf5117bfab192ea770",
2007
+                "url": "https://api.github.com/repos/filamentphp/tables/zipball/152bf46a8f2c46f047835771a67085c2866b039b",
2008
+                "reference": "152bf46a8f2c46f047835771a67085c2866b039b",
2009 2009
                 "shasum": ""
2010 2010
             },
2011 2011
             "require": {
@@ -2044,20 +2044,20 @@
2044 2044
                 "issues": "https://github.com/filamentphp/filament/issues",
2045 2045
                 "source": "https://github.com/filamentphp/filament"
2046 2046
             },
2047
-            "time": "2024-09-27T13:16:23+00:00"
2047
+            "time": "2024-10-08T14:24:25+00:00"
2048 2048
         },
2049 2049
         {
2050 2050
             "name": "filament/widgets",
2051
-            "version": "v3.2.115",
2051
+            "version": "v3.2.116",
2052 2052
             "source": {
2053 2053
                 "type": "git",
2054 2054
                 "url": "https://github.com/filamentphp/widgets.git",
2055
-                "reference": "06b70c4f260c91da03bdc51d1275543776ef7385"
2055
+                "reference": "14ae503aae8265ddc48274debbf7b7aefc7afb0b"
2056 2056
             },
2057 2057
             "dist": {
2058 2058
                 "type": "zip",
2059
-                "url": "https://api.github.com/repos/filamentphp/widgets/zipball/06b70c4f260c91da03bdc51d1275543776ef7385",
2060
-                "reference": "06b70c4f260c91da03bdc51d1275543776ef7385",
2059
+                "url": "https://api.github.com/repos/filamentphp/widgets/zipball/14ae503aae8265ddc48274debbf7b7aefc7afb0b",
2060
+                "reference": "14ae503aae8265ddc48274debbf7b7aefc7afb0b",
2061 2061
                 "shasum": ""
2062 2062
             },
2063 2063
             "require": {
@@ -2088,7 +2088,7 @@
2088 2088
                 "issues": "https://github.com/filamentphp/filament/issues",
2089 2089
                 "source": "https://github.com/filamentphp/filament"
2090 2090
             },
2091
-            "time": "2024-09-23T14:10:16+00:00"
2091
+            "time": "2024-10-08T14:24:26+00:00"
2092 2092
         },
2093 2093
         {
2094 2094
             "name": "firebase/php-jwt",
@@ -2774,27 +2774,28 @@
2774 2774
         },
2775 2775
         {
2776 2776
             "name": "kirschbaum-development/eloquent-power-joins",
2777
-            "version": "3.5.8",
2777
+            "version": "4.0.0",
2778 2778
             "source": {
2779 2779
                 "type": "git",
2780 2780
                 "url": "https://github.com/kirschbaum-development/eloquent-power-joins.git",
2781
-                "reference": "397ef08f15ceff48111fd7f57d9f1fd41bf1a453"
2781
+                "reference": "c6c42a52c5a097cc11761e72782b2d0215692caf"
2782 2782
             },
2783 2783
             "dist": {
2784 2784
                 "type": "zip",
2785
-                "url": "https://api.github.com/repos/kirschbaum-development/eloquent-power-joins/zipball/397ef08f15ceff48111fd7f57d9f1fd41bf1a453",
2786
-                "reference": "397ef08f15ceff48111fd7f57d9f1fd41bf1a453",
2785
+                "url": "https://api.github.com/repos/kirschbaum-development/eloquent-power-joins/zipball/c6c42a52c5a097cc11761e72782b2d0215692caf",
2786
+                "reference": "c6c42a52c5a097cc11761e72782b2d0215692caf",
2787 2787
                 "shasum": ""
2788 2788
             },
2789 2789
             "require": {
2790
-                "illuminate/database": "^8.0|^9.0|^10.0|^11.0",
2791
-                "illuminate/support": "^8.0|^9.0|^10.0|^11.0",
2792
-                "php": "^8.0"
2790
+                "illuminate/database": "^10.0|^11.0",
2791
+                "illuminate/support": "^10.0|^11.0",
2792
+                "php": "^8.1"
2793 2793
             },
2794 2794
             "require-dev": {
2795
+                "friendsofphp/php-cs-fixer": "dev-master",
2795 2796
                 "laravel/legacy-factories": "^1.0@dev",
2796
-                "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0|^9.0",
2797
-                "phpunit/phpunit": "^8.0|^9.0|^10.0"
2797
+                "orchestra/testbench": "^8.0|^9.0",
2798
+                "phpunit/phpunit": "^10.0"
2798 2799
             },
2799 2800
             "type": "library",
2800 2801
             "extra": {
@@ -2830,9 +2831,9 @@
2830 2831
             ],
2831 2832
             "support": {
2832 2833
                 "issues": "https://github.com/kirschbaum-development/eloquent-power-joins/issues",
2833
-                "source": "https://github.com/kirschbaum-development/eloquent-power-joins/tree/3.5.8"
2834
+                "source": "https://github.com/kirschbaum-development/eloquent-power-joins/tree/4.0.0"
2834 2835
             },
2835
-            "time": "2024-09-10T10:28:05+00:00"
2836
+            "time": "2024-10-06T12:28:14+00:00"
2836 2837
         },
2837 2838
         {
2838 2839
             "name": "knplabs/knp-snappy",
@@ -2903,16 +2904,16 @@
2903 2904
         },
2904 2905
         {
2905 2906
             "name": "laravel/framework",
2906
-            "version": "v11.25.0",
2907
+            "version": "v11.27.1",
2907 2908
             "source": {
2908 2909
                 "type": "git",
2909 2910
                 "url": "https://github.com/laravel/framework.git",
2910
-                "reference": "b487a9089c0b1c71ac63bb6bc44fb4b00dc6da2e"
2911
+                "reference": "2634ad4a6a71da3288943f058a8abbfec6a65ebb"
2911 2912
             },
2912 2913
             "dist": {
2913 2914
                 "type": "zip",
2914
-                "url": "https://api.github.com/repos/laravel/framework/zipball/b487a9089c0b1c71ac63bb6bc44fb4b00dc6da2e",
2915
-                "reference": "b487a9089c0b1c71ac63bb6bc44fb4b00dc6da2e",
2915
+                "url": "https://api.github.com/repos/laravel/framework/zipball/2634ad4a6a71da3288943f058a8abbfec6a65ebb",
2916
+                "reference": "2634ad4a6a71da3288943f058a8abbfec6a65ebb",
2916 2917
                 "shasum": ""
2917 2918
             },
2918 2919
             "require": {
@@ -2931,7 +2932,7 @@
2931 2932
                 "fruitcake/php-cors": "^1.3",
2932 2933
                 "guzzlehttp/guzzle": "^7.8",
2933 2934
                 "guzzlehttp/uri-template": "^1.0",
2934
-                "laravel/prompts": "^0.1.18|^0.2.0",
2935
+                "laravel/prompts": "^0.1.18|^0.2.0|^0.3.0",
2935 2936
                 "laravel/serializable-closure": "^1.3",
2936 2937
                 "league/commonmark": "^2.2.1",
2937 2938
                 "league/flysystem": "^3.8.0",
@@ -3108,7 +3109,7 @@
3108 3109
                 "issues": "https://github.com/laravel/framework/issues",
3109 3110
                 "source": "https://github.com/laravel/framework"
3110 3111
             },
3111
-            "time": "2024-09-26T11:21:58+00:00"
3112
+            "time": "2024-10-08T20:25:59+00:00"
3112 3113
         },
3113 3114
         {
3114 3115
             "name": "laravel/prompts",
@@ -3170,16 +3171,16 @@
3170 3171
         },
3171 3172
         {
3172 3173
             "name": "laravel/sanctum",
3173
-            "version": "v4.0.2",
3174
+            "version": "v4.0.3",
3174 3175
             "source": {
3175 3176
                 "type": "git",
3176 3177
                 "url": "https://github.com/laravel/sanctum.git",
3177
-                "reference": "9cfc0ce80cabad5334efff73ec856339e8ec1ac1"
3178
+                "reference": "54aea9d13743ae8a6cdd3c28dbef128a17adecab"
3178 3179
             },
3179 3180
             "dist": {
3180 3181
                 "type": "zip",
3181
-                "url": "https://api.github.com/repos/laravel/sanctum/zipball/9cfc0ce80cabad5334efff73ec856339e8ec1ac1",
3182
-                "reference": "9cfc0ce80cabad5334efff73ec856339e8ec1ac1",
3182
+                "url": "https://api.github.com/repos/laravel/sanctum/zipball/54aea9d13743ae8a6cdd3c28dbef128a17adecab",
3183
+                "reference": "54aea9d13743ae8a6cdd3c28dbef128a17adecab",
3183 3184
                 "shasum": ""
3184 3185
             },
3185 3186
             "require": {
@@ -3230,7 +3231,7 @@
3230 3231
                 "issues": "https://github.com/laravel/sanctum/issues",
3231 3232
                 "source": "https://github.com/laravel/sanctum"
3232 3233
             },
3233
-            "time": "2024-04-10T19:39:58+00:00"
3234
+            "time": "2024-09-27T14:55:41+00:00"
3234 3235
         },
3235 3236
         {
3236 3237
             "name": "laravel/serializable-closure",
@@ -3709,16 +3710,16 @@
3709 3710
         },
3710 3711
         {
3711 3712
             "name": "league/flysystem",
3712
-            "version": "3.29.0",
3713
+            "version": "3.29.1",
3713 3714
             "source": {
3714 3715
                 "type": "git",
3715 3716
                 "url": "https://github.com/thephpleague/flysystem.git",
3716
-                "reference": "0adc0d9a51852e170e0028a60bd271726626d3f0"
3717
+                "reference": "edc1bb7c86fab0776c3287dbd19b5fa278347319"
3717 3718
             },
3718 3719
             "dist": {
3719 3720
                 "type": "zip",
3720
-                "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/0adc0d9a51852e170e0028a60bd271726626d3f0",
3721
-                "reference": "0adc0d9a51852e170e0028a60bd271726626d3f0",
3721
+                "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/edc1bb7c86fab0776c3287dbd19b5fa278347319",
3722
+                "reference": "edc1bb7c86fab0776c3287dbd19b5fa278347319",
3722 3723
                 "shasum": ""
3723 3724
             },
3724 3725
             "require": {
@@ -3786,9 +3787,9 @@
3786 3787
             ],
3787 3788
             "support": {
3788 3789
                 "issues": "https://github.com/thephpleague/flysystem/issues",
3789
-                "source": "https://github.com/thephpleague/flysystem/tree/3.29.0"
3790
+                "source": "https://github.com/thephpleague/flysystem/tree/3.29.1"
3790 3791
             },
3791
-            "time": "2024-09-29T11:59:11+00:00"
3792
+            "time": "2024-10-08T08:58:34+00:00"
3792 3793
         },
3793 3794
         {
3794 3795
             "name": "league/flysystem-local",
@@ -4686,24 +4687,24 @@
4686 4687
         },
4687 4688
         {
4688 4689
             "name": "nette/schema",
4689
-            "version": "v1.3.0",
4690
+            "version": "v1.3.2",
4690 4691
             "source": {
4691 4692
                 "type": "git",
4692 4693
                 "url": "https://github.com/nette/schema.git",
4693
-                "reference": "a6d3a6d1f545f01ef38e60f375d1cf1f4de98188"
4694
+                "reference": "da801d52f0354f70a638673c4a0f04e16529431d"
4694 4695
             },
4695 4696
             "dist": {
4696 4697
                 "type": "zip",
4697
-                "url": "https://api.github.com/repos/nette/schema/zipball/a6d3a6d1f545f01ef38e60f375d1cf1f4de98188",
4698
-                "reference": "a6d3a6d1f545f01ef38e60f375d1cf1f4de98188",
4698
+                "url": "https://api.github.com/repos/nette/schema/zipball/da801d52f0354f70a638673c4a0f04e16529431d",
4699
+                "reference": "da801d52f0354f70a638673c4a0f04e16529431d",
4699 4700
                 "shasum": ""
4700 4701
             },
4701 4702
             "require": {
4702 4703
                 "nette/utils": "^4.0",
4703
-                "php": "8.1 - 8.3"
4704
+                "php": "8.1 - 8.4"
4704 4705
             },
4705 4706
             "require-dev": {
4706
-                "nette/tester": "^2.4",
4707
+                "nette/tester": "^2.5.2",
4707 4708
                 "phpstan/phpstan-nette": "^1.0",
4708 4709
                 "tracy/tracy": "^2.8"
4709 4710
             },
@@ -4742,9 +4743,9 @@
4742 4743
             ],
4743 4744
             "support": {
4744 4745
                 "issues": "https://github.com/nette/schema/issues",
4745
-                "source": "https://github.com/nette/schema/tree/v1.3.0"
4746
+                "source": "https://github.com/nette/schema/tree/v1.3.2"
4746 4747
             },
4747
-            "time": "2023-12-11T11:54:22+00:00"
4748
+            "time": "2024-10-06T23:10:23+00:00"
4748 4749
         },
4749 4750
         {
4750 4751
             "name": "nette/utils",
@@ -4834,16 +4835,16 @@
4834 4835
         },
4835 4836
         {
4836 4837
             "name": "nikic/php-parser",
4837
-            "version": "v5.3.0",
4838
+            "version": "v5.3.1",
4838 4839
             "source": {
4839 4840
                 "type": "git",
4840 4841
                 "url": "https://github.com/nikic/PHP-Parser.git",
4841
-                "reference": "3abf7425cd284141dc5d8d14a9ee444de3345d1a"
4842
+                "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b"
4842 4843
             },
4843 4844
             "dist": {
4844 4845
                 "type": "zip",
4845
-                "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3abf7425cd284141dc5d8d14a9ee444de3345d1a",
4846
-                "reference": "3abf7425cd284141dc5d8d14a9ee444de3345d1a",
4846
+                "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b",
4847
+                "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b",
4847 4848
                 "shasum": ""
4848 4849
             },
4849 4850
             "require": {
@@ -4886,9 +4887,9 @@
4886 4887
             ],
4887 4888
             "support": {
4888 4889
                 "issues": "https://github.com/nikic/PHP-Parser/issues",
4889
-                "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.0"
4890
+                "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1"
4890 4891
             },
4891
-            "time": "2024-09-29T13:56:26+00:00"
4892
+            "time": "2024-10-08T18:51:32+00:00"
4892 4893
         },
4893 4894
         {
4894 4895
             "name": "nunomaduro/termwind",
@@ -9154,16 +9155,16 @@
9154 9155
     "packages-dev": [
9155 9156
         {
9156 9157
             "name": "brianium/paratest",
9157
-            "version": "v7.5.5",
9158
+            "version": "v7.5.7",
9158 9159
             "source": {
9159 9160
                 "type": "git",
9160 9161
                 "url": "https://github.com/paratestphp/paratest.git",
9161
-                "reference": "f29c7d671afc5c4e1140bd7b9f2749e827902a1e"
9162
+                "reference": "4890b17f569efea5e034e519dc883da53a67448d"
9162 9163
             },
9163 9164
             "dist": {
9164 9165
                 "type": "zip",
9165
-                "url": "https://api.github.com/repos/paratestphp/paratest/zipball/f29c7d671afc5c4e1140bd7b9f2749e827902a1e",
9166
-                "reference": "f29c7d671afc5c4e1140bd7b9f2749e827902a1e",
9166
+                "url": "https://api.github.com/repos/paratestphp/paratest/zipball/4890b17f569efea5e034e519dc883da53a67448d",
9167
+                "reference": "4890b17f569efea5e034e519dc883da53a67448d",
9167 9168
                 "shasum": ""
9168 9169
             },
9169 9170
             "require": {
@@ -9177,22 +9178,22 @@
9177 9178
                 "phpunit/php-code-coverage": "^11.0.6",
9178 9179
                 "phpunit/php-file-iterator": "^5.1.0",
9179 9180
                 "phpunit/php-timer": "^7.0.1",
9180
-                "phpunit/phpunit": "^11.3.6",
9181
+                "phpunit/phpunit": "^11.4.0",
9181 9182
                 "sebastian/environment": "^7.2.0",
9182
-                "symfony/console": "^6.4.11 || ^7.1.4",
9183
-                "symfony/process": "^6.4.8 || ^7.1.3"
9183
+                "symfony/console": "^6.4.11 || ^7.1.5",
9184
+                "symfony/process": "^6.4.8 || ^7.1.5"
9184 9185
             },
9185 9186
             "require-dev": {
9186 9187
                 "doctrine/coding-standard": "^12.0.0",
9187 9188
                 "ext-pcov": "*",
9188 9189
                 "ext-posix": "*",
9189
-                "infection/infection": "^0.29.6",
9190
-                "phpstan/phpstan": "^1.12.4",
9190
+                "infection/infection": "^0.29.7",
9191
+                "phpstan/phpstan": "^1.12.6",
9191 9192
                 "phpstan/phpstan-deprecation-rules": "^1.2.1",
9192 9193
                 "phpstan/phpstan-phpunit": "^1.4.0",
9193
-                "phpstan/phpstan-strict-rules": "^1.6.0",
9194
+                "phpstan/phpstan-strict-rules": "^1.6.1",
9194 9195
                 "squizlabs/php_codesniffer": "^3.10.3",
9195
-                "symfony/filesystem": "^6.4.9 || ^7.1.2"
9196
+                "symfony/filesystem": "^6.4.9 || ^7.1.5"
9196 9197
             },
9197 9198
             "bin": [
9198 9199
                 "bin/paratest",
@@ -9232,7 +9233,7 @@
9232 9233
             ],
9233 9234
             "support": {
9234 9235
                 "issues": "https://github.com/paratestphp/paratest/issues",
9235
-                "source": "https://github.com/paratestphp/paratest/tree/v7.5.5"
9236
+                "source": "https://github.com/paratestphp/paratest/tree/v7.5.7"
9236 9237
             },
9237 9238
             "funding": [
9238 9239
                 {
@@ -9244,7 +9245,7 @@
9244 9245
                     "type": "paypal"
9245 9246
                 }
9246 9247
             ],
9247
-            "time": "2024-09-20T12:57:46+00:00"
9248
+            "time": "2024-10-07T06:27:54+00:00"
9248 9249
         },
9249 9250
         {
9250 9251
             "name": "fakerphp/faker",
@@ -9619,16 +9620,16 @@
9619 9620
         },
9620 9621
         {
9621 9622
             "name": "laravel/sail",
9622
-            "version": "v1.33.0",
9623
+            "version": "v1.35.0",
9623 9624
             "source": {
9624 9625
                 "type": "git",
9625 9626
                 "url": "https://github.com/laravel/sail.git",
9626
-                "reference": "d54af9d5745e3680d8a6463ffd9f314aa53eb2d1"
9627
+                "reference": "992bc2d9e52174c79515967f30849d21daa334d8"
9627 9628
             },
9628 9629
             "dist": {
9629 9630
                 "type": "zip",
9630
-                "url": "https://api.github.com/repos/laravel/sail/zipball/d54af9d5745e3680d8a6463ffd9f314aa53eb2d1",
9631
-                "reference": "d54af9d5745e3680d8a6463ffd9f314aa53eb2d1",
9631
+                "url": "https://api.github.com/repos/laravel/sail/zipball/992bc2d9e52174c79515967f30849d21daa334d8",
9632
+                "reference": "992bc2d9e52174c79515967f30849d21daa334d8",
9632 9633
                 "shasum": ""
9633 9634
             },
9634 9635
             "require": {
@@ -9678,7 +9679,7 @@
9678 9679
                 "issues": "https://github.com/laravel/sail/issues",
9679 9680
                 "source": "https://github.com/laravel/sail"
9680 9681
             },
9681
-            "time": "2024-09-22T19:04:21+00:00"
9682
+            "time": "2024-10-08T14:45:26+00:00"
9682 9683
         },
9683 9684
         {
9684 9685
             "name": "mockery/mockery",
@@ -9922,36 +9923,36 @@
9922 9923
         },
9923 9924
         {
9924 9925
             "name": "pestphp/pest",
9925
-            "version": "v3.2.4",
9926
+            "version": "v3.3.0",
9926 9927
             "source": {
9927 9928
                 "type": "git",
9928 9929
                 "url": "https://github.com/pestphp/pest.git",
9929
-                "reference": "5fe79d9c18a674e9cce2f36f365516c26ae87b49"
9930
+                "reference": "0a7bff0d246b10040e12e4152215e12a599e742a"
9930 9931
             },
9931 9932
             "dist": {
9932 9933
                 "type": "zip",
9933
-                "url": "https://api.github.com/repos/pestphp/pest/zipball/5fe79d9c18a674e9cce2f36f365516c26ae87b49",
9934
-                "reference": "5fe79d9c18a674e9cce2f36f365516c26ae87b49",
9934
+                "url": "https://api.github.com/repos/pestphp/pest/zipball/0a7bff0d246b10040e12e4152215e12a599e742a",
9935
+                "reference": "0a7bff0d246b10040e12e4152215e12a599e742a",
9935 9936
                 "shasum": ""
9936 9937
             },
9937 9938
             "require": {
9938
-                "brianium/paratest": "^7.5.5",
9939
+                "brianium/paratest": "^7.5.6",
9939 9940
                 "nunomaduro/collision": "^8.4.0",
9940 9941
                 "nunomaduro/termwind": "^2.1.0",
9941 9942
                 "pestphp/pest-plugin": "^3.0.0",
9942 9943
                 "pestphp/pest-plugin-arch": "^3.0.0",
9943 9944
                 "pestphp/pest-plugin-mutate": "^3.0.5",
9944 9945
                 "php": "^8.2.0",
9945
-                "phpunit/phpunit": "^11.3.6"
9946
+                "phpunit/phpunit": "^11.4.0"
9946 9947
             },
9947 9948
             "conflict": {
9948
-                "phpunit/phpunit": ">11.3.6",
9949
+                "phpunit/phpunit": ">11.4.0",
9949 9950
                 "sebastian/exporter": "<6.0.0",
9950 9951
                 "webmozart/assert": "<1.11.0"
9951 9952
             },
9952 9953
             "require-dev": {
9953 9954
                 "pestphp/pest-dev-tools": "^3.0.0",
9954
-                "pestphp/pest-plugin-type-coverage": "^3.0.0",
9955
+                "pestphp/pest-plugin-type-coverage": "^3.1.0",
9955 9956
                 "symfony/process": "^7.1.5"
9956 9957
             },
9957 9958
             "bin": [
@@ -10017,7 +10018,7 @@
10017 10018
             ],
10018 10019
             "support": {
10019 10020
                 "issues": "https://github.com/pestphp/pest/issues",
10020
-                "source": "https://github.com/pestphp/pest/tree/v3.2.4"
10021
+                "source": "https://github.com/pestphp/pest/tree/v3.3.0"
10021 10022
             },
10022 10023
             "funding": [
10023 10024
                 {
@@ -10029,7 +10030,7 @@
10029 10030
                     "type": "github"
10030 10031
                 }
10031 10032
             ],
10032
-            "time": "2024-09-26T22:53:39+00:00"
10033
+            "time": "2024-10-06T18:25:27+00:00"
10033 10034
         },
10034 10035
         {
10035 10036
             "name": "pestphp/pest-plugin",
@@ -10779,16 +10780,16 @@
10779 10780
         },
10780 10781
         {
10781 10782
             "name": "phpstan/phpstan",
10782
-            "version": "1.12.5",
10783
+            "version": "1.12.6",
10783 10784
             "source": {
10784 10785
                 "type": "git",
10785 10786
                 "url": "https://github.com/phpstan/phpstan.git",
10786
-                "reference": "7e6c6cb7cecb0a6254009a1a8a7d54ec99812b17"
10787
+                "reference": "dc4d2f145a88ea7141ae698effd64d9df46527ae"
10787 10788
             },
10788 10789
             "dist": {
10789 10790
                 "type": "zip",
10790
-                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/7e6c6cb7cecb0a6254009a1a8a7d54ec99812b17",
10791
-                "reference": "7e6c6cb7cecb0a6254009a1a8a7d54ec99812b17",
10791
+                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc4d2f145a88ea7141ae698effd64d9df46527ae",
10792
+                "reference": "dc4d2f145a88ea7141ae698effd64d9df46527ae",
10792 10793
                 "shasum": ""
10793 10794
             },
10794 10795
             "require": {
@@ -10833,7 +10834,7 @@
10833 10834
                     "type": "github"
10834 10835
                 }
10835 10836
             ],
10836
-            "time": "2024-09-26T12:45:22+00:00"
10837
+            "time": "2024-10-06T15:03:59+00:00"
10837 10838
         },
10838 10839
         {
10839 10840
             "name": "phpunit/php-code-coverage",
@@ -11160,16 +11161,16 @@
11160 11161
         },
11161 11162
         {
11162 11163
             "name": "phpunit/phpunit",
11163
-            "version": "11.3.6",
11164
+            "version": "11.4.0",
11164 11165
             "source": {
11165 11166
                 "type": "git",
11166 11167
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
11167
-                "reference": "d62c45a19c665bb872c2a47023a0baf41a98bb2b"
11168
+                "reference": "89fe0c530133c08f7fff89d3d727154e4e504925"
11168 11169
             },
11169 11170
             "dist": {
11170 11171
                 "type": "zip",
11171
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d62c45a19c665bb872c2a47023a0baf41a98bb2b",
11172
-                "reference": "d62c45a19c665bb872c2a47023a0baf41a98bb2b",
11172
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/89fe0c530133c08f7fff89d3d727154e4e504925",
11173
+                "reference": "89fe0c530133c08f7fff89d3d727154e4e504925",
11173 11174
                 "shasum": ""
11174 11175
             },
11175 11176
             "require": {
@@ -11208,7 +11209,7 @@
11208 11209
             "type": "library",
11209 11210
             "extra": {
11210 11211
                 "branch-alias": {
11211
-                    "dev-main": "11.3-dev"
11212
+                    "dev-main": "11.4-dev"
11212 11213
                 }
11213 11214
             },
11214 11215
             "autoload": {
@@ -11240,7 +11241,7 @@
11240 11241
             "support": {
11241 11242
                 "issues": "https://github.com/sebastianbergmann/phpunit/issues",
11242 11243
                 "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
11243
-                "source": "https://github.com/sebastianbergmann/phpunit/tree/11.3.6"
11244
+                "source": "https://github.com/sebastianbergmann/phpunit/tree/11.4.0"
11244 11245
             },
11245 11246
             "funding": [
11246 11247
                 {
@@ -11256,25 +11257,25 @@
11256 11257
                     "type": "tidelift"
11257 11258
                 }
11258 11259
             ],
11259
-            "time": "2024-09-19T10:54:28+00:00"
11260
+            "time": "2024-10-05T08:39:03+00:00"
11260 11261
         },
11261 11262
         {
11262 11263
             "name": "rector/rector",
11263
-            "version": "1.2.5",
11264
+            "version": "1.2.6",
11264 11265
             "source": {
11265 11266
                 "type": "git",
11266 11267
                 "url": "https://github.com/rectorphp/rector.git",
11267
-                "reference": "e98aa793ca3fcd17e893cfaf9103ac049775d339"
11268
+                "reference": "6ca85da28159dbd3bb36211c5104b7bc91278e99"
11268 11269
             },
11269 11270
             "dist": {
11270 11271
                 "type": "zip",
11271
-                "url": "https://api.github.com/repos/rectorphp/rector/zipball/e98aa793ca3fcd17e893cfaf9103ac049775d339",
11272
-                "reference": "e98aa793ca3fcd17e893cfaf9103ac049775d339",
11272
+                "url": "https://api.github.com/repos/rectorphp/rector/zipball/6ca85da28159dbd3bb36211c5104b7bc91278e99",
11273
+                "reference": "6ca85da28159dbd3bb36211c5104b7bc91278e99",
11273 11274
                 "shasum": ""
11274 11275
             },
11275 11276
             "require": {
11276 11277
                 "php": "^7.2|^8.0",
11277
-                "phpstan/phpstan": "^1.12.2"
11278
+                "phpstan/phpstan": "^1.12.5"
11278 11279
             },
11279 11280
             "conflict": {
11280 11281
                 "rector/rector-doctrine": "*",
@@ -11307,7 +11308,7 @@
11307 11308
             ],
11308 11309
             "support": {
11309 11310
                 "issues": "https://github.com/rectorphp/rector/issues",
11310
-                "source": "https://github.com/rectorphp/rector/tree/1.2.5"
11311
+                "source": "https://github.com/rectorphp/rector/tree/1.2.6"
11311 11312
             },
11312 11313
             "funding": [
11313 11314
                 {
@@ -11315,7 +11316,7 @@
11315 11316
                     "type": "github"
11316 11317
                 }
11317 11318
             ],
11318
-            "time": "2024-09-08T17:43:24+00:00"
11319
+            "time": "2024-10-03T08:56:44+00:00"
11319 11320
         },
11320 11321
         {
11321 11322
             "name": "sebastian/cli-parser",

+ 41
- 1
database/factories/Accounting/AccountFactory.php 查看文件

@@ -3,6 +3,10 @@
3 3
 namespace Database\Factories\Accounting;
4 4
 
5 5
 use App\Models\Accounting\Account;
6
+use App\Models\Accounting\AccountSubtype;
7
+use App\Models\Banking\BankAccount;
8
+use App\Models\Setting\Currency;
9
+use App\Utilities\Currency\CurrencyAccessor;
6 10
 use Illuminate\Database\Eloquent\Factories\Factory;
7 11
 
8 12
 /**
@@ -23,7 +27,43 @@ class AccountFactory extends Factory
23 27
     public function definition(): array
24 28
     {
25 29
         return [
26
-            //
30
+            'company_id' => 1,
31
+            'subtype_id' => 1,
32
+            'name' => $this->faker->unique()->word,
33
+            'currency_code' => CurrencyAccessor::getDefaultCurrency() ?? 'USD',
34
+            'description' => $this->faker->sentence,
35
+            'archived' => false,
36
+            'default' => false,
27 37
         ];
28 38
     }
39
+
40
+    public function withBankAccount(string $name): static
41
+    {
42
+        return $this->state(function (array $attributes) use ($name) {
43
+            $bankAccount = BankAccount::factory()->create();
44
+            $accountSubtype = AccountSubtype::where('name', 'Cash and Cash Equivalents')->first();
45
+
46
+            return [
47
+                'bank_account_id' => $bankAccount->id,
48
+                'subtype_id' => $accountSubtype->id,
49
+                'name' => $name,
50
+            ];
51
+        });
52
+    }
53
+
54
+    public function withForeignBankAccount(string $name, string $currencyCode, float $rate): static
55
+    {
56
+        return $this->state(function (array $attributes) use ($currencyCode, $rate, $name) {
57
+            $currency = Currency::factory()->forCurrency($currencyCode, $rate)->create();
58
+            $bankAccount = BankAccount::factory()->create();
59
+            $accountSubtype = AccountSubtype::where('name', 'Cash and Cash Equivalents')->first();
60
+
61
+            return [
62
+                'bank_account_id' => $bankAccount->id,
63
+                'subtype_id' => $accountSubtype->id,
64
+                'name' => $name,
65
+                'currency_code' => $currency->code,
66
+            ];
67
+        });
68
+    }
29 69
 }

+ 54
- 11
database/factories/Accounting/TransactionFactory.php 查看文件

@@ -9,9 +9,7 @@ use App\Models\Accounting\Transaction;
9 9
 use App\Models\Banking\BankAccount;
10 10
 use App\Models\Company;
11 11
 use App\Models\Setting\CompanyDefault;
12
-use App\Services\TransactionService;
13 12
 use Illuminate\Database\Eloquent\Factories\Factory;
14
-use Illuminate\Support\Facades\DB;
15 13
 
16 14
 /**
17 15
  * @extends Factory<Transaction>
@@ -45,15 +43,6 @@ class TransactionFactory extends Factory
45 43
         ];
46 44
     }
47 45
 
48
-    public function configure(): static
49
-    {
50
-        return $this->afterCreating(function (Transaction $transaction) {
51
-            if (DB::getDefaultConnection() === 'sqlite') {
52
-                app(TransactionService::class)->createJournalEntries($transaction);
53
-            }
54
-        });
55
-    }
56
-
57 46
     public function forCompanyAndBankAccount(Company $company, BankAccount $bankAccount): static
58 47
     {
59 48
         return $this->state(function (array $attributes) use ($bankAccount, $company) {
@@ -100,6 +89,28 @@ class TransactionFactory extends Factory
100 89
         });
101 90
     }
102 91
 
92
+    public function forBankAccount(?BankAccount $bankAccount = null): static
93
+    {
94
+        return $this->state(function (array $attributes) use ($bankAccount) {
95
+            $bankAccount = $bankAccount ?? BankAccount::factory()->create();
96
+
97
+            return [
98
+                'bank_account_id' => $bankAccount->id,
99
+            ];
100
+        });
101
+    }
102
+
103
+    public function forDestinationBankAccount(?Account $account = null): static
104
+    {
105
+        return $this->state(function (array $attributes) use ($account) {
106
+            $destinationBankAccount = $account ?? Account::factory()->withBankAccount('Destination Bank Account')->create();
107
+
108
+            return [
109
+                'account_id' => $destinationBankAccount->id,
110
+            ];
111
+        });
112
+    }
113
+
103 114
     public function forUncategorizedRevenue(): static
104 115
     {
105 116
         return $this->state(function (array $attributes) {
@@ -122,6 +133,18 @@ class TransactionFactory extends Factory
122 133
         });
123 134
     }
124 135
 
136
+    public function forAccount(Account $account): static
137
+    {
138
+        return $this->state([
139
+            'account_id' => $account->id,
140
+        ]);
141
+    }
142
+
143
+    public function forType(TransactionType $type, int $amount): static
144
+    {
145
+        return $this->state(compact('type', 'amount'));
146
+    }
147
+
125 148
     public function asDeposit(int $amount): static
126 149
     {
127 150
         return $this->state(function () use ($amount) {
@@ -141,4 +164,24 @@ class TransactionFactory extends Factory
141 164
             ];
142 165
         });
143 166
     }
167
+
168
+    public function asJournal(int $amount): static
169
+    {
170
+        return $this->state(function () use ($amount) {
171
+            return [
172
+                'type' => TransactionType::Journal,
173
+                'amount' => $amount,
174
+            ];
175
+        });
176
+    }
177
+
178
+    public function asTransfer(int $amount): static
179
+    {
180
+        return $this->state(function () use ($amount) {
181
+            return [
182
+                'type' => TransactionType::Transfer,
183
+                'amount' => $amount,
184
+            ];
185
+        });
186
+    }
144 187
 }

+ 10
- 1
database/factories/Banking/BankAccountFactory.php 查看文件

@@ -2,6 +2,7 @@
2 2
 
3 3
 namespace Database\Factories\Banking;
4 4
 
5
+use App\Enums\Banking\BankAccountType;
5 6
 use App\Models\Banking\BankAccount;
6 7
 use Illuminate\Database\Eloquent\Factories\Factory;
7 8
 
@@ -10,6 +11,11 @@ use Illuminate\Database\Eloquent\Factories\Factory;
10 11
  */
11 12
 class BankAccountFactory extends Factory
12 13
 {
14
+    /**
15
+     * The name of the factory's corresponding model.
16
+     */
17
+    protected $model = BankAccount::class;
18
+
13 19
     /**
14 20
      * Define the model's default state.
15 21
      *
@@ -18,7 +24,10 @@ class BankAccountFactory extends Factory
18 24
     public function definition(): array
19 25
     {
20 26
         return [
21
-            //
27
+            'company_id' => 1,
28
+            'type' => BankAccountType::Depository,
29
+            'number' => $this->faker->unique()->numerify(str_repeat('#', 12)),
30
+            'enabled' => false,
22 31
         ];
23 32
     }
24 33
 }

+ 7
- 10
database/factories/CompanyFactory.php 查看文件

@@ -2,13 +2,12 @@
2 2
 
3 3
 namespace Database\Factories;
4 4
 
5
+use App\Models\Accounting\Transaction;
5 6
 use App\Models\Company;
6 7
 use App\Models\Setting\CompanyProfile;
7 8
 use App\Models\User;
8 9
 use App\Services\CompanyDefaultService;
9
-use Database\Factories\Accounting\TransactionFactory;
10 10
 use Illuminate\Database\Eloquent\Factories\Factory;
11
-use Illuminate\Support\Facades\DB;
12 11
 
13 12
 class CompanyFactory extends Factory
14 13
 {
@@ -36,7 +35,7 @@ class CompanyFactory extends Factory
36 35
     public function withCompanyProfile(): self
37 36
     {
38 37
         return $this->afterCreating(function (Company $company) {
39
-            CompanyProfile::factory()->forCompany($company)->create();
38
+            CompanyProfile::factory()->forCompany($company)->withCountry('US')->create();
40 39
         });
41 40
     }
42 41
 
@@ -46,11 +45,9 @@ class CompanyFactory extends Factory
46 45
     public function withCompanyDefaults(): self
47 46
     {
48 47
         return $this->afterCreating(function (Company $company) {
49
-            DB::transaction(function () use ($company) {
50
-                $countryCode = $company->profile->country;
51
-                $companyDefaultService = app(CompanyDefaultService::class);
52
-                $companyDefaultService->createCompanyDefaults($company, $company->owner, 'USD', $countryCode, 'en');
53
-            });
48
+            $countryCode = $company->profile->country;
49
+            $companyDefaultService = app(CompanyDefaultService::class);
50
+            $companyDefaultService->createCompanyDefaults($company, $company->owner, 'USD', $countryCode, 'en');
54 51
         });
55 52
     }
56 53
 
@@ -59,10 +56,10 @@ class CompanyFactory extends Factory
59 56
         return $this->afterCreating(function (Company $company) use ($count) {
60 57
             $defaultBankAccount = $company->default->bankAccount;
61 58
 
62
-            TransactionFactory::new()
59
+            Transaction::factory()
63 60
                 ->forCompanyAndBankAccount($company, $defaultBankAccount)
64 61
                 ->count($count)
65
-                ->createQuietly([
62
+                ->create([
66 63
                     'created_by' => $company->user_id,
67 64
                     'updated_by' => $company->user_id,
68 65
                 ]);

+ 9
- 9
database/factories/Setting/CompanyDefaultFactory.php 查看文件

@@ -70,7 +70,7 @@ class CompanyDefaultFactory extends Factory
70 70
 
71 71
     private function createCurrency(Company $company, User $user, string $currencyCode): Currency
72 72
     {
73
-        return Currency::factory()->forCurrency($currencyCode)->create([
73
+        return Currency::factory()->forCurrency($currencyCode)->createQuietly([
74 74
             'company_id' => $company->id,
75 75
             'created_by' => $user->id,
76 76
             'updated_by' => $user->id,
@@ -79,7 +79,7 @@ class CompanyDefaultFactory extends Factory
79 79
 
80 80
     private function createSalesTax(Company $company, User $user): Tax
81 81
     {
82
-        return Tax::factory()->salesTax()->create([
82
+        return Tax::factory()->salesTax()->createQuietly([
83 83
             'company_id' => $company->id,
84 84
             'created_by' => $user->id,
85 85
             'updated_by' => $user->id,
@@ -88,7 +88,7 @@ class CompanyDefaultFactory extends Factory
88 88
 
89 89
     private function createPurchaseTax(Company $company, User $user): Tax
90 90
     {
91
-        return Tax::factory()->purchaseTax()->create([
91
+        return Tax::factory()->purchaseTax()->createQuietly([
92 92
             'company_id' => $company->id,
93 93
             'created_by' => $user->id,
94 94
             'updated_by' => $user->id,
@@ -97,7 +97,7 @@ class CompanyDefaultFactory extends Factory
97 97
 
98 98
     private function createSalesDiscount(Company $company, User $user): Discount
99 99
     {
100
-        return Discount::factory()->salesDiscount()->create([
100
+        return Discount::factory()->salesDiscount()->createQuietly([
101 101
             'company_id' => $company->id,
102 102
             'created_by' => $user->id,
103 103
             'updated_by' => $user->id,
@@ -106,7 +106,7 @@ class CompanyDefaultFactory extends Factory
106 106
 
107 107
     private function createPurchaseDiscount(Company $company, User $user): Discount
108 108
     {
109
-        return Discount::factory()->purchaseDiscount()->create([
109
+        return Discount::factory()->purchaseDiscount()->createQuietly([
110 110
             'company_id' => $company->id,
111 111
             'created_by' => $user->id,
112 112
             'updated_by' => $user->id,
@@ -115,7 +115,7 @@ class CompanyDefaultFactory extends Factory
115 115
 
116 116
     private function createAppearance(Company $company, User $user): void
117 117
     {
118
-        Appearance::factory()->create([
118
+        Appearance::factory()->createQuietly([
119 119
             'company_id' => $company->id,
120 120
             'created_by' => $user->id,
121 121
             'updated_by' => $user->id,
@@ -124,13 +124,13 @@ class CompanyDefaultFactory extends Factory
124 124
 
125 125
     private function createDocumentDefaults(Company $company, User $user): void
126 126
     {
127
-        DocumentDefault::factory()->invoice()->create([
127
+        DocumentDefault::factory()->invoice()->createQuietly([
128 128
             'company_id' => $company->id,
129 129
             'created_by' => $user->id,
130 130
             'updated_by' => $user->id,
131 131
         ]);
132 132
 
133
-        DocumentDefault::factory()->bill()->create([
133
+        DocumentDefault::factory()->bill()->createQuietly([
134 134
             'company_id' => $company->id,
135 135
             'created_by' => $user->id,
136 136
             'updated_by' => $user->id,
@@ -139,7 +139,7 @@ class CompanyDefaultFactory extends Factory
139 139
 
140 140
     private function createLocalization(Company $company, User $user, string $countryCode, string $language): void
141 141
     {
142
-        Localization::factory()->withCountry($countryCode, $language)->create([
142
+        Localization::factory()->withCountry($countryCode, $language)->createQuietly([
143 143
             'company_id' => $company->id,
144 144
             'created_by' => $user->id,
145 145
             'updated_by' => $user->id,

+ 2
- 2
database/factories/Setting/CurrencyFactory.php 查看文件

@@ -40,14 +40,14 @@ class CurrencyFactory extends Factory
40 40
     /**
41 41
      * Define a state for a specific currency.
42 42
      */
43
-    public function forCurrency(string $code): Factory
43
+    public function forCurrency(string $code, ?float $rate = null): static
44 44
     {
45 45
         $currency = currency($code);
46 46
 
47 47
         return $this->state([
48 48
             'name' => $currency->getName(),
49 49
             'code' => $currency->getCurrency(),
50
-            'rate' => $currency->getRate(),
50
+            'rate' => $rate ?? $currency->getRate(),
51 51
             'precision' => $currency->getPrecision(),
52 52
             'symbol' => $currency->getSymbol(),
53 53
             'symbol_first' => $currency->isSymbolFirst(),

+ 15
- 6
database/factories/Setting/DiscountFactory.php 查看文件

@@ -7,6 +7,7 @@ use App\Enums\Setting\DiscountScope;
7 7
 use App\Enums\Setting\DiscountType;
8 8
 use App\Models\Setting\Discount;
9 9
 use Illuminate\Database\Eloquent\Factories\Factory;
10
+use Illuminate\Support\Carbon;
10 11
 
11 12
 /**
12 13
  * @extends Factory<Discount>
@@ -26,16 +27,26 @@ class DiscountFactory extends Factory
26 27
     public function definition(): array
27 28
     {
28 29
         $startDate = $this->faker->dateTimeBetween('now', '+1 year');
29
-        $endDate = $this->faker->dateTimeBetween($startDate, strtotime('+1 year'));
30
+        $endDate = $this->faker->dateTimeBetween($startDate, Carbon::parse($startDate)->addYear());
31
+
32
+        $computation = $this->faker->randomElement(DiscountComputation::class);
33
+
34
+        if ($computation === DiscountComputation::Fixed) {
35
+            $rate = $this->faker->numberBetween(5, 100) * 100; // $5 - $100
36
+        } else {
37
+            $rate = $this->faker->numberBetween(3, 50) * 10000; // 3% - 50%
38
+        }
30 39
 
31 40
         return [
41
+            'name' => $this->faker->unique()->word,
32 42
             'description' => $this->faker->sentence,
33
-            'rate' => $this->faker->biasedNumberBetween(300, 5000) * 100, // 3% - 50%
34
-            'computation' => $this->faker->randomElement(DiscountComputation::class),
43
+            'rate' => $rate,
44
+            'computation' => $computation,
45
+            'type' => $this->faker->randomElement(DiscountType::class),
35 46
             'scope' => $this->faker->randomElement(DiscountScope::class),
36 47
             'start_date' => $startDate,
37 48
             'end_date' => $endDate,
38
-            'enabled' => true,
49
+            'enabled' => false,
39 50
         ];
40 51
     }
41 52
 
@@ -43,7 +54,6 @@ class DiscountFactory extends Factory
43 54
     {
44 55
         return $this->state([
45 56
             'name' => 'Summer Sale',
46
-            'rate' => $this->faker->biasedNumberBetween(300, 1200) * 100, // 3% - 12%
47 57
             'type' => DiscountType::Sales,
48 58
         ]);
49 59
     }
@@ -52,7 +62,6 @@ class DiscountFactory extends Factory
52 62
     {
53 63
         return $this->state([
54 64
             'name' => 'Bulk Purchase',
55
-            'rate' => $this->faker->biasedNumberBetween(300, 1200) * 100, // 3% - 12%
56 65
             'type' => DiscountType::Purchase,
57 66
         ]);
58 67
     }

+ 13
- 5
database/factories/Setting/TaxFactory.php 查看文件

@@ -25,12 +25,22 @@ class TaxFactory extends Factory
25 25
      */
26 26
     public function definition(): array
27 27
     {
28
+        $computation = $this->faker->randomElement(TaxComputation::class);
29
+
30
+        if ($computation === TaxComputation::Fixed) {
31
+            $rate = $this->faker->biasedNumberBetween(1, 10) * 100; // $1 - $10
32
+        } else {
33
+            $rate = $this->faker->biasedNumberBetween(3, 25) * 10000; // 3% - 25%
34
+        }
35
+
28 36
         return [
37
+            'name' => $this->faker->unique()->word,
29 38
             'description' => $this->faker->sentence,
30
-            'rate' => $this->faker->biasedNumberBetween(300, 5000) * 100, // 3% - 50%
31
-            'computation' => $this->faker->randomElement(TaxComputation::class),
39
+            'rate' => $rate,
40
+            'computation' => $computation,
41
+            'type' => $this->faker->randomElement(TaxType::class),
32 42
             'scope' => $this->faker->randomElement(TaxScope::class),
33
-            'enabled' => true,
43
+            'enabled' => false,
34 44
         ];
35 45
     }
36 46
 
@@ -38,7 +48,6 @@ class TaxFactory extends Factory
38 48
     {
39 49
         return $this->state([
40 50
             'name' => 'State Sales Tax',
41
-            'rate' => $this->faker->biasedNumberBetween(300, 1200) * 100, // 3% - 12%
42 51
             'type' => TaxType::Sales,
43 52
         ]);
44 53
     }
@@ -47,7 +56,6 @@ class TaxFactory extends Factory
47 56
     {
48 57
         return $this->state([
49 58
             'name' => 'State Purchase Tax',
50
-            'rate' => $this->faker->biasedNumberBetween(300, 1200) * 100, // 3% - 12%
51 59
             'type' => TaxType::Purchase,
52 60
         ]);
53 61
     }

+ 6
- 9
database/seeders/DatabaseSeeder.php 查看文件

@@ -14,22 +14,19 @@ class DatabaseSeeder extends Seeder
14 14
     public function run(): void
15 15
     {
16 16
         // Create a single admin user and their personal company
17
-        $adminUser = User::factory()
17
+        User::factory()
18 18
             ->withPersonalCompany(function (CompanyFactory $factory) {
19
-                return $factory->withTransactions();
19
+                return $factory
20
+                    ->state([
21
+                        'name' => 'ERPSAAS',
22
+                    ])
23
+                    ->withTransactions();
20 24
             })
21 25
             ->create([
22 26
                 'name' => 'Admin',
23 27
                 'email' => 'admin@gmail.com',
24 28
                 'password' => bcrypt('password'),
25 29
                 'current_company_id' => 1,  // Assuming this will be the ID of the created company
26
-                'created_at' => now(),
27 30
             ]);
28
-
29
-        // Optionally, set additional properties or create related entities specific to this company
30
-        $adminUser->ownedCompanies->first()->update([
31
-            'name' => 'ERPSAAS',
32
-            'created_at' => now(),
33
-        ]);
34 31
     }
35 32
 }

+ 4
- 1
database/seeders/TestDatabaseSeeder.php 查看文件

@@ -3,15 +3,18 @@
3 3
 namespace Database\Seeders;
4 4
 
5 5
 use App\Models\User;
6
+use Illuminate\Database\Console\Seeds\WithoutModelEvents;
6 7
 use Illuminate\Database\Seeder;
7 8
 
8 9
 class TestDatabaseSeeder extends Seeder
9 10
 {
11
+    use WithoutModelEvents;
12
+
10 13
     public function run(): void
11 14
     {
12 15
         User::factory()
13 16
             ->withPersonalCompany()
14
-            ->createQuietly([
17
+            ->create([
15 18
                 'name' => 'Test Company Owner',
16 19
                 'email' => 'test@gmail.com',
17 20
                 'password' => bcrypt('password'),

+ 73
- 73
package-lock.json 查看文件

@@ -541,9 +541,9 @@
541 541
             }
542 542
         },
543 543
         "node_modules/@rollup/rollup-android-arm-eabi": {
544
-            "version": "4.22.5",
545
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.5.tgz",
546
-            "integrity": "sha512-SU5cvamg0Eyu/F+kLeMXS7GoahL+OoizlclVFX3l5Ql6yNlywJJ0OuqTzUx0v+aHhPHEB/56CT06GQrRrGNYww==",
544
+            "version": "4.24.0",
545
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz",
546
+            "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==",
547 547
             "cpu": [
548 548
                 "arm"
549 549
             ],
@@ -555,9 +555,9 @@
555 555
             ]
556 556
         },
557 557
         "node_modules/@rollup/rollup-android-arm64": {
558
-            "version": "4.22.5",
559
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.5.tgz",
560
-            "integrity": "sha512-S4pit5BP6E5R5C8S6tgU/drvgjtYW76FBuG6+ibG3tMvlD1h9LHVF9KmlmaUBQ8Obou7hEyS+0w+IR/VtxwNMQ==",
558
+            "version": "4.24.0",
559
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz",
560
+            "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==",
561 561
             "cpu": [
562 562
                 "arm64"
563 563
             ],
@@ -569,9 +569,9 @@
569 569
             ]
570 570
         },
571 571
         "node_modules/@rollup/rollup-darwin-arm64": {
572
-            "version": "4.22.5",
573
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.5.tgz",
574
-            "integrity": "sha512-250ZGg4ipTL0TGvLlfACkIxS9+KLtIbn7BCZjsZj88zSg2Lvu3Xdw6dhAhfe/FjjXPVNCtcSp+WZjVsD3a/Zlw==",
572
+            "version": "4.24.0",
573
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz",
574
+            "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==",
575 575
             "cpu": [
576 576
                 "arm64"
577 577
             ],
@@ -583,9 +583,9 @@
583 583
             ]
584 584
         },
585 585
         "node_modules/@rollup/rollup-darwin-x64": {
586
-            "version": "4.22.5",
587
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.5.tgz",
588
-            "integrity": "sha512-D8brJEFg5D+QxFcW6jYANu+Rr9SlKtTenmsX5hOSzNYVrK5oLAEMTUgKWYJP+wdKyCdeSwnapLsn+OVRFycuQg==",
586
+            "version": "4.24.0",
587
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz",
588
+            "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==",
589 589
             "cpu": [
590 590
                 "x64"
591 591
             ],
@@ -597,9 +597,9 @@
597 597
             ]
598 598
         },
599 599
         "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
600
-            "version": "4.22.5",
601
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.5.tgz",
602
-            "integrity": "sha512-PNqXYmdNFyWNg0ma5LdY8wP+eQfdvyaBAojAXgO7/gs0Q/6TQJVXAXe8gwW9URjbS0YAammur0fynYGiWsKlXw==",
600
+            "version": "4.24.0",
601
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz",
602
+            "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==",
603 603
             "cpu": [
604 604
                 "arm"
605 605
             ],
@@ -611,9 +611,9 @@
611 611
             ]
612 612
         },
613 613
         "node_modules/@rollup/rollup-linux-arm-musleabihf": {
614
-            "version": "4.22.5",
615
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.5.tgz",
616
-            "integrity": "sha512-kSSCZOKz3HqlrEuwKd9TYv7vxPYD77vHSUvM2y0YaTGnFc8AdI5TTQRrM1yIp3tXCKrSL9A7JLoILjtad5t8pQ==",
614
+            "version": "4.24.0",
615
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz",
616
+            "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==",
617 617
             "cpu": [
618 618
                 "arm"
619 619
             ],
@@ -625,9 +625,9 @@
625 625
             ]
626 626
         },
627 627
         "node_modules/@rollup/rollup-linux-arm64-gnu": {
628
-            "version": "4.22.5",
629
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.5.tgz",
630
-            "integrity": "sha512-oTXQeJHRbOnwRnRffb6bmqmUugz0glXaPyspp4gbQOPVApdpRrY/j7KP3lr7M8kTfQTyrBUzFjj5EuHAhqH4/w==",
628
+            "version": "4.24.0",
629
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz",
630
+            "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==",
631 631
             "cpu": [
632 632
                 "arm64"
633 633
             ],
@@ -639,9 +639,9 @@
639 639
             ]
640 640
         },
641 641
         "node_modules/@rollup/rollup-linux-arm64-musl": {
642
-            "version": "4.22.5",
643
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.5.tgz",
644
-            "integrity": "sha512-qnOTIIs6tIGFKCHdhYitgC2XQ2X25InIbZFor5wh+mALH84qnFHvc+vmWUpyX97B0hNvwNUL4B+MB8vJvH65Fw==",
642
+            "version": "4.24.0",
643
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz",
644
+            "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==",
645 645
             "cpu": [
646 646
                 "arm64"
647 647
             ],
@@ -653,9 +653,9 @@
653 653
             ]
654 654
         },
655 655
         "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
656
-            "version": "4.22.5",
657
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.5.tgz",
658
-            "integrity": "sha512-TMYu+DUdNlgBXING13rHSfUc3Ky5nLPbWs4bFnT+R6Vu3OvXkTkixvvBKk8uO4MT5Ab6lC3U7x8S8El2q5o56w==",
656
+            "version": "4.24.0",
657
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz",
658
+            "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==",
659 659
             "cpu": [
660 660
                 "ppc64"
661 661
             ],
@@ -667,9 +667,9 @@
667 667
             ]
668 668
         },
669 669
         "node_modules/@rollup/rollup-linux-riscv64-gnu": {
670
-            "version": "4.22.5",
671
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.5.tgz",
672
-            "integrity": "sha512-PTQq1Kz22ZRvuhr3uURH+U/Q/a0pbxJoICGSprNLAoBEkyD3Sh9qP5I0Asn0y0wejXQBbsVMRZRxlbGFD9OK4A==",
670
+            "version": "4.24.0",
671
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz",
672
+            "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==",
673 673
             "cpu": [
674 674
                 "riscv64"
675 675
             ],
@@ -681,9 +681,9 @@
681 681
             ]
682 682
         },
683 683
         "node_modules/@rollup/rollup-linux-s390x-gnu": {
684
-            "version": "4.22.5",
685
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.5.tgz",
686
-            "integrity": "sha512-bR5nCojtpuMss6TDEmf/jnBnzlo+6n1UhgwqUvRoe4VIotC7FG1IKkyJbwsT7JDsF2jxR+NTnuOwiGv0hLyDoQ==",
684
+            "version": "4.24.0",
685
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz",
686
+            "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==",
687 687
             "cpu": [
688 688
                 "s390x"
689 689
             ],
@@ -695,9 +695,9 @@
695 695
             ]
696 696
         },
697 697
         "node_modules/@rollup/rollup-linux-x64-gnu": {
698
-            "version": "4.22.5",
699
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.5.tgz",
700
-            "integrity": "sha512-N0jPPhHjGShcB9/XXZQWuWBKZQnC1F36Ce3sDqWpujsGjDz/CQtOL9LgTrJ+rJC8MJeesMWrMWVLKKNR/tMOCA==",
698
+            "version": "4.24.0",
699
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz",
700
+            "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==",
701 701
             "cpu": [
702 702
                 "x64"
703 703
             ],
@@ -709,9 +709,9 @@
709 709
             ]
710 710
         },
711 711
         "node_modules/@rollup/rollup-linux-x64-musl": {
712
-            "version": "4.22.5",
713
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.5.tgz",
714
-            "integrity": "sha512-uBa2e28ohzNNwjr6Uxm4XyaA1M/8aTgfF2T7UIlElLaeXkgpmIJ2EitVNQxjO9xLLLy60YqAgKn/AqSpCUkE9g==",
712
+            "version": "4.24.0",
713
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz",
714
+            "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==",
715 715
             "cpu": [
716 716
                 "x64"
717 717
             ],
@@ -723,9 +723,9 @@
723 723
             ]
724 724
         },
725 725
         "node_modules/@rollup/rollup-win32-arm64-msvc": {
726
-            "version": "4.22.5",
727
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.5.tgz",
728
-            "integrity": "sha512-RXT8S1HP8AFN/Kr3tg4fuYrNxZ/pZf1HemC5Tsddc6HzgGnJm0+Lh5rAHJkDuW3StI0ynNXukidROMXYl6ew8w==",
726
+            "version": "4.24.0",
727
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz",
728
+            "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==",
729 729
             "cpu": [
730 730
                 "arm64"
731 731
             ],
@@ -737,9 +737,9 @@
737 737
             ]
738 738
         },
739 739
         "node_modules/@rollup/rollup-win32-ia32-msvc": {
740
-            "version": "4.22.5",
741
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.5.tgz",
742
-            "integrity": "sha512-ElTYOh50InL8kzyUD6XsnPit7jYCKrphmddKAe1/Ytt74apOxDq5YEcbsiKs0fR3vff3jEneMM+3I7jbqaMyBg==",
740
+            "version": "4.24.0",
741
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz",
742
+            "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==",
743 743
             "cpu": [
744 744
                 "ia32"
745 745
             ],
@@ -751,9 +751,9 @@
751 751
             ]
752 752
         },
753 753
         "node_modules/@rollup/rollup-win32-x64-msvc": {
754
-            "version": "4.22.5",
755
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.5.tgz",
756
-            "integrity": "sha512-+lvL/4mQxSV8MukpkKyyvfwhH266COcWlXE/1qxwN08ajovta3459zrjLghYMgDerlzNwLAcFpvU+WWE5y6nAQ==",
754
+            "version": "4.24.0",
755
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz",
756
+            "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==",
757 757
             "cpu": [
758 758
                 "x64"
759 759
             ],
@@ -998,9 +998,9 @@
998 998
             }
999 999
         },
1000 1000
         "node_modules/caniuse-lite": {
1001
-            "version": "1.0.30001664",
1002
-            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001664.tgz",
1003
-            "integrity": "sha512-AmE7k4dXiNKQipgn7a2xg558IRqPN3jMQY/rOsbxDhrd0tyChwbITBfiwtnqz8bi2M5mIWbxAYBvk7W7QBUS2g==",
1001
+            "version": "1.0.30001667",
1002
+            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz",
1003
+            "integrity": "sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==",
1004 1004
             "dev": true,
1005 1005
             "funding": [
1006 1006
                 {
@@ -1159,9 +1159,9 @@
1159 1159
             "license": "MIT"
1160 1160
         },
1161 1161
         "node_modules/electron-to-chromium": {
1162
-            "version": "1.5.30",
1163
-            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.30.tgz",
1164
-            "integrity": "sha512-sXI35EBN4lYxzc/pIGorlymYNzDBOqkSlVRe6MkgBsW/hW1tpC/HDJ2fjG7XnjakzfLEuvdmux0Mjs6jHq4UOA==",
1162
+            "version": "1.5.33",
1163
+            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.33.tgz",
1164
+            "integrity": "sha512-+cYTcFB1QqD4j4LegwLfpCNxifb6dDFUAwk6RsLusCwIaZI6or2f+q8rs5tTB2YC53HhOlIbEaqHMAAC8IOIwA==",
1165 1165
             "dev": true,
1166 1166
             "license": "ISC"
1167 1167
         },
@@ -2171,9 +2171,9 @@
2171 2171
             }
2172 2172
         },
2173 2173
         "node_modules/rollup": {
2174
-            "version": "4.22.5",
2175
-            "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.5.tgz",
2176
-            "integrity": "sha512-WoinX7GeQOFMGznEcWA1WrTQCd/tpEbMkc3nuMs9BT0CPjMdSjPMTVClwWd4pgSQwJdP65SK9mTCNvItlr5o7w==",
2174
+            "version": "4.24.0",
2175
+            "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz",
2176
+            "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==",
2177 2177
             "dev": true,
2178 2178
             "license": "MIT",
2179 2179
             "dependencies": {
@@ -2187,22 +2187,22 @@
2187 2187
                 "npm": ">=8.0.0"
2188 2188
             },
2189 2189
             "optionalDependencies": {
2190
-                "@rollup/rollup-android-arm-eabi": "4.22.5",
2191
-                "@rollup/rollup-android-arm64": "4.22.5",
2192
-                "@rollup/rollup-darwin-arm64": "4.22.5",
2193
-                "@rollup/rollup-darwin-x64": "4.22.5",
2194
-                "@rollup/rollup-linux-arm-gnueabihf": "4.22.5",
2195
-                "@rollup/rollup-linux-arm-musleabihf": "4.22.5",
2196
-                "@rollup/rollup-linux-arm64-gnu": "4.22.5",
2197
-                "@rollup/rollup-linux-arm64-musl": "4.22.5",
2198
-                "@rollup/rollup-linux-powerpc64le-gnu": "4.22.5",
2199
-                "@rollup/rollup-linux-riscv64-gnu": "4.22.5",
2200
-                "@rollup/rollup-linux-s390x-gnu": "4.22.5",
2201
-                "@rollup/rollup-linux-x64-gnu": "4.22.5",
2202
-                "@rollup/rollup-linux-x64-musl": "4.22.5",
2203
-                "@rollup/rollup-win32-arm64-msvc": "4.22.5",
2204
-                "@rollup/rollup-win32-ia32-msvc": "4.22.5",
2205
-                "@rollup/rollup-win32-x64-msvc": "4.22.5",
2190
+                "@rollup/rollup-android-arm-eabi": "4.24.0",
2191
+                "@rollup/rollup-android-arm64": "4.24.0",
2192
+                "@rollup/rollup-darwin-arm64": "4.24.0",
2193
+                "@rollup/rollup-darwin-x64": "4.24.0",
2194
+                "@rollup/rollup-linux-arm-gnueabihf": "4.24.0",
2195
+                "@rollup/rollup-linux-arm-musleabihf": "4.24.0",
2196
+                "@rollup/rollup-linux-arm64-gnu": "4.24.0",
2197
+                "@rollup/rollup-linux-arm64-musl": "4.24.0",
2198
+                "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0",
2199
+                "@rollup/rollup-linux-riscv64-gnu": "4.24.0",
2200
+                "@rollup/rollup-linux-s390x-gnu": "4.24.0",
2201
+                "@rollup/rollup-linux-x64-gnu": "4.24.0",
2202
+                "@rollup/rollup-linux-x64-musl": "4.24.0",
2203
+                "@rollup/rollup-win32-arm64-msvc": "4.24.0",
2204
+                "@rollup/rollup-win32-ia32-msvc": "4.24.0",
2205
+                "@rollup/rollup-win32-x64-msvc": "4.24.0",
2206 2206
                 "fsevents": "~2.3.2"
2207 2207
             }
2208 2208
         },

+ 2
- 2
resources/views/components/panel-shift-dropdown.blade.php 查看文件

@@ -61,11 +61,11 @@
61 61
                         <x-panel-shift-dropdown.content-handler :item="$item"/>
62 62
                     @endforeach
63 63
                 @endif
64
-                @if($panelId === 'company-settings')
64
+                @if($panelId === 'company-settings' && $currentTenant)
65 65
                     <x-panel-shift-dropdown.company-settings :current-tenant="$currentTenant"
66 66
                                                              icon="heroicon-m-building-office-2"/>
67 67
                 @endif
68
-                @if($panelId === 'company-switcher')
68
+                @if($panelId === 'company-switcher' && $currentTenant)
69 69
                     <x-panel-shift-dropdown.company-switcher :current-tenant="$currentTenant"
70 70
                                                              icon="heroicon-m-adjustments-horizontal"/>
71 71
                 @endif

+ 515
- 0
tests/Feature/Accounting/TransactionTest.php 查看文件

@@ -0,0 +1,515 @@
1
+<?php
2
+
3
+use App\Enums\Accounting\JournalEntryType;
4
+use App\Enums\Accounting\TransactionType;
5
+use App\Filament\Company\Pages\Accounting\Transactions;
6
+use App\Filament\Forms\Components\JournalEntryRepeater;
7
+use App\Filament\Tables\Actions\ReplicateBulkAction;
8
+use App\Models\Accounting\Account;
9
+use App\Models\Accounting\Transaction;
10
+use App\Utilities\Currency\ConfigureCurrencies;
11
+use Filament\Tables\Actions\DeleteAction;
12
+use Filament\Tables\Actions\DeleteBulkAction;
13
+use Filament\Tables\Actions\ReplicateAction;
14
+
15
+use function Pest\Livewire\livewire;
16
+
17
+it('creates correct journal entries for a deposit transaction', function () {
18
+    $transaction = Transaction::factory()
19
+        ->forDefaultBankAccount()
20
+        ->forUncategorizedRevenue()
21
+        ->asDeposit(1000)
22
+        ->create();
23
+
24
+    [$debitAccount, $creditAccount] = getTransactionDebitAndCreditAccounts($transaction);
25
+
26
+    expect($transaction->journalEntries->count())->toBe(2)
27
+        ->and($debitAccount->name)->toBe('Cash on Hand')
28
+        ->and($creditAccount->name)->toBe('Uncategorized Income');
29
+});
30
+
31
+it('creates correct journal entries for a withdrawal transaction', function () {
32
+    $transaction = Transaction::factory()
33
+        ->forDefaultBankAccount()
34
+        ->forUncategorizedExpense()
35
+        ->asWithdrawal(500)
36
+        ->create();
37
+
38
+    [$debitAccount, $creditAccount] = getTransactionDebitAndCreditAccounts($transaction);
39
+
40
+    expect($transaction->journalEntries->count())->toBe(2)
41
+        ->and($debitAccount->name)->toBe('Uncategorized Expense')
42
+        ->and($creditAccount->name)->toBe('Cash on Hand');
43
+});
44
+
45
+it('creates correct journal entries for a transfer transaction', function () {
46
+    $transaction = Transaction::factory()
47
+        ->forDefaultBankAccount()
48
+        ->forDestinationBankAccount()
49
+        ->asTransfer(1500)
50
+        ->create();
51
+
52
+    [$debitAccount, $creditAccount] = getTransactionDebitAndCreditAccounts($transaction);
53
+
54
+    // Acts as a withdrawal transaction for the source account
55
+    expect($transaction->journalEntries->count())->toBe(2)
56
+        ->and($debitAccount->name)->toBe('Destination Bank Account')
57
+        ->and($creditAccount->name)->toBe('Cash on Hand');
58
+});
59
+
60
+it('does not create journal entries for a journal transaction', function () {
61
+    $transaction = Transaction::factory()
62
+        ->forDefaultBankAccount()
63
+        ->forUncategorizedRevenue()
64
+        ->asJournal(1000)
65
+        ->create();
66
+
67
+    // Journal entries for a journal transaction are created manually
68
+    expect($transaction->journalEntries->count())->toBe(0);
69
+});
70
+
71
+it('stores and sums correct debit and credit amounts for different transaction types', function ($method, $setupMethod, $amount) {
72
+    /** @var Transaction $transaction */
73
+    $transaction = Transaction::factory()
74
+        ->forDefaultBankAccount()
75
+        ->{$setupMethod}()
76
+        ->{$method}($amount)
77
+        ->create();
78
+
79
+    expect($transaction)
80
+        ->journalEntries->sumDebits()->getValue()->toEqual($amount)
81
+        ->journalEntries->sumCredits()->getValue()->toEqual($amount);
82
+})->with([
83
+    ['asDeposit', 'forUncategorizedRevenue', 2000],
84
+    ['asWithdrawal', 'forUncategorizedExpense', 500],
85
+    ['asTransfer', 'forDestinationBankAccount', 1500],
86
+]);
87
+
88
+it('deletes associated journal entries when transaction is deleted', function () {
89
+    $transaction = Transaction::factory()
90
+        ->forDefaultBankAccount()
91
+        ->forUncategorizedRevenue()
92
+        ->asDeposit(1000)
93
+        ->create();
94
+
95
+    expect($transaction->journalEntries()->count())->toBe(2);
96
+
97
+    $transaction->delete();
98
+
99
+    $this->assertModelMissing($transaction);
100
+
101
+    $this->assertDatabaseCount('journal_entries', 0);
102
+});
103
+
104
+it('handles multi-currency transfers without conversion when the source bank account is in the default currency', function () {
105
+    $foreignBankAccount = Account::factory()
106
+        ->withForeignBankAccount('Foreign Bank Account', 'EUR', 0.92)
107
+        ->create();
108
+
109
+    /** @var Transaction $transaction */
110
+    $transaction = Transaction::factory()
111
+        ->forDefaultBankAccount()
112
+        ->forDestinationBankAccount($foreignBankAccount)
113
+        ->asTransfer(1500)
114
+        ->create();
115
+
116
+    [$debitAccount, $creditAccount] = getTransactionDebitAndCreditAccounts($transaction);
117
+
118
+    expect($debitAccount->is($foreignBankAccount))->toBeTrue()
119
+        ->and($creditAccount->name)->toBe('Cash on Hand');
120
+
121
+    $expectedUSDValue = 1500;
122
+
123
+    expect($transaction)
124
+        ->amount->toEqual('1,500.00')
125
+        ->journalEntries->count()->toBe(2)
126
+        ->journalEntries->sumDebits()->getValue()->toEqual($expectedUSDValue)
127
+        ->journalEntries->sumCredits()->getValue()->toEqual($expectedUSDValue);
128
+});
129
+
130
+it('handles multi-currency transfers correctly', function () {
131
+    $foreignBankAccount = Account::factory()
132
+        ->withForeignBankAccount('CAD Bank Account', 'CAD', 1.36)
133
+        ->create();
134
+
135
+    ConfigureCurrencies::syncCurrencies();
136
+
137
+    // Create a transfer of 1500 CAD from the foreign bank account to USD bank account
138
+    /** @var Transaction $transaction */
139
+    $transaction = Transaction::factory()
140
+        ->forBankAccount($foreignBankAccount->bankAccount)
141
+        ->forDestinationBankAccount()
142
+        ->asTransfer(1500)
143
+        ->create();
144
+
145
+    [$debitAccount, $creditAccount] = getTransactionDebitAndCreditAccounts($transaction);
146
+
147
+    expect($debitAccount->name)->toBe('Destination Bank Account') // Debit: Destination (USD) account
148
+        ->and($creditAccount->is($foreignBankAccount))->toBeTrue(); // Credit: Foreign Bank Account (CAD) account
149
+
150
+    // The 1500 CAD is worth 1102.94 USD (1500 CAD / 1.36)
151
+    $expectedUSDValue = round(1500 / 1.36, 2);
152
+
153
+    // Verify that the debit is 1102.94 USD and the credit is 1500 CAD converted to 1102.94 USD
154
+    // Transaction amount stays in source bank account currency (cast is applied)
155
+    expect($transaction)
156
+        ->amount->toEqual('1,500.00')
157
+        ->journalEntries->count()->toBe(2)
158
+        ->journalEntries->sumDebits()->getValue()->toEqual($expectedUSDValue)
159
+        ->journalEntries->sumCredits()->getValue()->toEqual($expectedUSDValue);
160
+});
161
+
162
+it('handles multi-currency deposits correctly', function () {
163
+    $foreignBankAccount = Account::factory()
164
+        ->withForeignBankAccount('BHD Bank Account', 'BHD', 0.38)
165
+        ->create();
166
+
167
+    ConfigureCurrencies::syncCurrencies();
168
+
169
+    // Create a deposit of 1500 BHD to the foreign bank account
170
+    /** @var Transaction $transaction */
171
+    $transaction = Transaction::factory()
172
+        ->forBankAccount($foreignBankAccount->bankAccount)
173
+        ->forUncategorizedRevenue()
174
+        ->asDeposit(1500)
175
+        ->create();
176
+
177
+    [$debitAccount, $creditAccount] = getTransactionDebitAndCreditAccounts($transaction);
178
+
179
+    expect($debitAccount->is($foreignBankAccount))->toBeTrue() // Debit: Foreign Bank Account (BHD) account
180
+        ->and($creditAccount->name)->toBe('Uncategorized Income'); // Credit: Uncategorized Income (USD) account
181
+
182
+    // Convert to USD using the rate 0.38 BHD per USD
183
+    $expectedUSDValue = round(1500 / 0.38, 2);
184
+
185
+    // Verify that the debit is 39473.68 USD and the credit is 1500 BHD converted to 39473.68 USD
186
+    expect($transaction)
187
+        ->amount->toEqual('1,500.000')
188
+        ->journalEntries->count()->toBe(2)
189
+        ->journalEntries->sumDebits()->getValue()->toEqual($expectedUSDValue)
190
+        ->journalEntries->sumCredits()->getValue()->toEqual($expectedUSDValue);
191
+});
192
+
193
+it('handles multi-currency withdrawals correctly', function () {
194
+    $foreignBankAccount = Account::factory()
195
+        ->withForeignBankAccount('Foreign Bank Account', 'GBP', 0.76) // GBP account
196
+        ->create();
197
+
198
+    ConfigureCurrencies::syncCurrencies();
199
+
200
+    /** @var Transaction $transaction */
201
+    $transaction = Transaction::factory()
202
+        ->forBankAccount($foreignBankAccount->bankAccount)
203
+        ->forUncategorizedExpense()
204
+        ->asWithdrawal(1500)
205
+        ->create();
206
+
207
+    [$debitAccount, $creditAccount] = getTransactionDebitAndCreditAccounts($transaction);
208
+
209
+    expect($debitAccount->name)->toBe('Uncategorized Expense')
210
+        ->and($creditAccount->is($foreignBankAccount))->toBeTrue();
211
+
212
+    $expectedUSDValue = round(1500 / 0.76, 2);
213
+
214
+    expect($transaction)
215
+        ->amount->toEqual('1,500.00')
216
+        ->journalEntries->count()->toBe(2)
217
+        ->journalEntries->sumDebits()->getValue()->toEqual($expectedUSDValue)
218
+        ->journalEntries->sumCredits()->getValue()->toEqual($expectedUSDValue);
219
+});
220
+
221
+it('can add an income or expense transaction', function (TransactionType $transactionType, string $actionName) {
222
+    $testCompany = $this->testCompany;
223
+    $defaultBankAccount = $testCompany->default->bankAccount;
224
+    $defaultAccount = Transactions::getUncategorizedAccountByType($transactionType);
225
+
226
+    livewire(Transactions::class)
227
+        ->mountAction($actionName)
228
+        ->assertActionDataSet([
229
+            'posted_at' => now()->toDateTimeString(),
230
+            'type' => $transactionType,
231
+            'bank_account_id' => $defaultBankAccount->id,
232
+            'amount' => '0.00',
233
+            'account_id' => $defaultAccount->id,
234
+        ])
235
+        ->setActionData([
236
+            'amount' => '500.00',
237
+        ])
238
+        ->callMountedAction()
239
+        ->assertHasNoActionErrors();
240
+
241
+    $transaction = Transaction::first();
242
+
243
+    expect($transaction)
244
+        ->not->toBeNull()
245
+        ->amount->toEqual('500.00')
246
+        ->type->toBe($transactionType)
247
+        ->bankAccount->is($defaultBankAccount)->toBeTrue()
248
+        ->account->is($defaultAccount)->toBeTrue()
249
+        ->journalEntries->count()->toBe(2);
250
+})->with([
251
+    [TransactionType::Deposit, 'addIncome'],
252
+    [TransactionType::Withdrawal, 'addExpense'],
253
+]);
254
+
255
+it('can add a transfer transaction', function () {
256
+    $testCompany = $this->testCompany;
257
+    $sourceBankAccount = $testCompany->default->bankAccount;
258
+    $destinationBankAccount = Account::factory()->withBankAccount('Destination Bank Account')->create();
259
+
260
+    livewire(Transactions::class)
261
+        ->mountAction('addTransfer')
262
+        ->assertActionDataSet([
263
+            'posted_at' => now()->toDateTimeString(),
264
+            'type' => TransactionType::Transfer,
265
+            'bank_account_id' => $sourceBankAccount->id,
266
+            'amount' => '0.00',
267
+            'account_id' => null,
268
+        ])
269
+        ->setActionData([
270
+            'account_id' => $destinationBankAccount->id,
271
+            'amount' => '1,500.00',
272
+        ])
273
+        ->callMountedAction()
274
+        ->assertHasNoActionErrors();
275
+
276
+    $transaction = Transaction::first();
277
+
278
+    expect($transaction)
279
+        ->not->toBeNull()
280
+        ->amount->toEqual('1,500.00')
281
+        ->type->toBe(TransactionType::Transfer)
282
+        ->bankAccount->is($sourceBankAccount)->toBeTrue()
283
+        ->account->is($destinationBankAccount)->toBeTrue()
284
+        ->journalEntries->count()->toBe(2);
285
+});
286
+
287
+it('can add a journal transaction', function () {
288
+    $defaultDebitAccount = Transactions::getUncategorizedAccountByType(TransactionType::Withdrawal);
289
+    $defaultCreditAccount = Transactions::getUncategorizedAccountByType(TransactionType::Deposit);
290
+
291
+    $undoRepeaterFake = JournalEntryRepeater::fake();
292
+
293
+    livewire(Transactions::class)
294
+        ->mountAction('addJournalTransaction')
295
+        ->assertActionDataSet([
296
+            'posted_at' => now()->toDateTimeString(),
297
+            'journalEntries' => [
298
+                ['type' => JournalEntryType::Debit, 'account_id' => $defaultDebitAccount->id, 'amount' => '0.00'],
299
+                ['type' => JournalEntryType::Credit, 'account_id' => $defaultCreditAccount->id, 'amount' => '0.00'],
300
+            ],
301
+        ])
302
+        ->setActionData([
303
+            'journalEntries' => [
304
+                ['amount' => '1,000.00'],
305
+                ['amount' => '1,000.00'],
306
+            ],
307
+        ])
308
+        ->callMountedAction()
309
+        ->assertHasNoActionErrors();
310
+
311
+    $undoRepeaterFake();
312
+
313
+    $transaction = Transaction::first();
314
+
315
+    [$debitAccount, $creditAccount] = getTransactionDebitAndCreditAccounts($transaction);
316
+
317
+    expect($transaction)
318
+        ->not->toBeNull()
319
+        ->amount->toEqual('1,000.00')
320
+        ->type->isJournal()->toBeTrue()
321
+        ->bankAccount->toBeNull()
322
+        ->account->toBeNull()
323
+        ->journalEntries->count()->toBe(2)
324
+        ->journalEntries->sumDebits()->getValue()->toEqual(1000)
325
+        ->journalEntries->sumCredits()->getValue()->toEqual(1000)
326
+        ->and($debitAccount->is($defaultDebitAccount))->toBeTrue()
327
+        ->and($creditAccount->is($defaultCreditAccount))->toBeTrue();
328
+});
329
+
330
+it('can update a deposit or withdrawal transaction', function (TransactionType $transactionType) {
331
+    $defaultAccount = Transactions::getUncategorizedAccountByType($transactionType);
332
+
333
+    $transaction = Transaction::factory()
334
+        ->forDefaultBankAccount()
335
+        ->forAccount($defaultAccount)
336
+        ->forType($transactionType, 1000)
337
+        ->create();
338
+
339
+    $newDescription = 'Updated Description';
340
+
341
+    livewire(Transactions::class)
342
+        ->mountTableAction('updateTransaction', $transaction)
343
+        ->assertTableActionDataSet([
344
+            'type' => $transactionType->value,
345
+            'description' => $transaction->description,
346
+            'amount' => $transaction->amount,
347
+        ])
348
+        ->setTableActionData([
349
+            'description' => $newDescription,
350
+            'amount' => '1,500.00',
351
+        ])
352
+        ->callMountedTableAction()
353
+        ->assertHasNoTableActionErrors();
354
+
355
+    $transaction->refresh();
356
+
357
+    expect($transaction->description)->toBe($newDescription)
358
+        ->and($transaction->amount)->toEqual('1,500.00');
359
+})->with([
360
+    TransactionType::Deposit,
361
+    TransactionType::Withdrawal,
362
+]);
363
+
364
+it('does not show Edit Transfer or Edit Journal Transaction for deposit or withdrawal transactions', function (TransactionType $transactionType) {
365
+    $defaultAccount = Transactions::getUncategorizedAccountByType($transactionType);
366
+
367
+    $transaction = Transaction::factory()
368
+        ->forDefaultBankAccount()
369
+        ->forAccount($defaultAccount)
370
+        ->forType($transactionType, 1000)
371
+        ->create();
372
+
373
+    livewire(Transactions::class)
374
+        ->assertTableActionHidden('updateTransfer', $transaction)
375
+        ->assertTableActionHidden('updateJournalTransaction', $transaction);
376
+})->with([
377
+    TransactionType::Deposit,
378
+    TransactionType::Withdrawal,
379
+]);
380
+
381
+it('can update a transfer transaction', function () {
382
+    $transaction = Transaction::factory()
383
+        ->forDefaultBankAccount()
384
+        ->forDestinationBankAccount()
385
+        ->asTransfer(1500)
386
+        ->create();
387
+
388
+    $newDescription = 'Updated Transfer Description';
389
+
390
+    livewire(Transactions::class)
391
+        ->mountTableAction('updateTransfer', $transaction)
392
+        ->assertTableActionDataSet([
393
+            'type' => TransactionType::Transfer->value,
394
+            'description' => $transaction->description,
395
+            'amount' => $transaction->amount,
396
+        ])
397
+        ->setTableActionData([
398
+            'description' => $newDescription,
399
+            'amount' => '2,000.00',
400
+        ])
401
+        ->callMountedTableAction()
402
+        ->assertHasNoTableActionErrors();
403
+
404
+    $transaction->refresh();
405
+
406
+    expect($transaction->description)->toBe($newDescription)
407
+        ->and($transaction->amount)->toEqual('2,000.00');
408
+});
409
+
410
+it('does not show Edit Transaction or Edit Journal Transaction for transfer transactions', function () {
411
+    $transaction = Transaction::factory()
412
+        ->forDefaultBankAccount()
413
+        ->forDestinationBankAccount()
414
+        ->asTransfer(1500)
415
+        ->create();
416
+
417
+    livewire(Transactions::class)
418
+        ->assertTableActionHidden('updateTransaction', $transaction)
419
+        ->assertTableActionHidden('updateJournalTransaction', $transaction);
420
+});
421
+
422
+it('replicates a transaction with correct journal entries', function () {
423
+    $originalTransaction = Transaction::factory()
424
+        ->forDefaultBankAccount()
425
+        ->forUncategorizedRevenue()
426
+        ->asDeposit(1000)
427
+        ->create();
428
+
429
+    livewire(Transactions::class)
430
+        ->callTableAction(ReplicateAction::class, $originalTransaction);
431
+
432
+    $replicatedTransaction = Transaction::whereKeyNot($originalTransaction->getKey())->first();
433
+
434
+    expect($replicatedTransaction)->not->toBeNull();
435
+
436
+    [$originalDebitAccount, $originalCreditAccount] = getTransactionDebitAndCreditAccounts($originalTransaction);
437
+
438
+    [$replicatedDebitAccount, $replicatedCreditAccount] = getTransactionDebitAndCreditAccounts($replicatedTransaction);
439
+
440
+    expect($replicatedTransaction)
441
+        ->journalEntries->count()->toBe(2)
442
+        ->journalEntries->sumDebits()->getValue()->toEqual(1000)
443
+        ->journalEntries->sumCredits()->getValue()->toEqual(1000)
444
+        ->description->toBe('(Copy of) ' . $originalTransaction->description)
445
+        ->and($replicatedDebitAccount->name)->toBe($originalDebitAccount->name)
446
+        ->and($replicatedCreditAccount->name)->toBe($originalCreditAccount->name);
447
+});
448
+
449
+it('bulk replicates transactions with correct journal entries', function () {
450
+    $originalTransactions = Transaction::factory()
451
+        ->forDefaultBankAccount()
452
+        ->forUncategorizedRevenue()
453
+        ->asDeposit(1000)
454
+        ->count(3)
455
+        ->create();
456
+
457
+    livewire(Transactions::class)
458
+        ->callTableBulkAction(ReplicateBulkAction::class, $originalTransactions);
459
+
460
+    $replicatedTransactions = Transaction::whereKeyNot($originalTransactions->modelKeys())->get();
461
+
462
+    expect($replicatedTransactions->count())->toBe(3);
463
+
464
+    $originalTransactions->each(function (Transaction $originalTransaction) use ($replicatedTransactions) {
465
+        /** @var Transaction $replicatedTransaction */
466
+        $replicatedTransaction = $replicatedTransactions->firstWhere('description', '(Copy of) ' . $originalTransaction->description);
467
+
468
+        expect($replicatedTransaction)->not->toBeNull();
469
+
470
+        [$originalDebitAccount, $originalCreditAccount] = getTransactionDebitAndCreditAccounts($originalTransaction);
471
+
472
+        [$replicatedDebitAccount, $replicatedCreditAccount] = getTransactionDebitAndCreditAccounts($replicatedTransaction);
473
+
474
+        expect($replicatedTransaction)
475
+            ->journalEntries->count()->toBe(2)
476
+            ->journalEntries->sumDebits()->getValue()->toEqual(1000)
477
+            ->journalEntries->sumCredits()->getValue()->toEqual(1000)
478
+            ->and($replicatedDebitAccount->name)->toBe($originalDebitAccount->name)
479
+            ->and($replicatedCreditAccount->name)->toBe($originalCreditAccount->name);
480
+    });
481
+});
482
+
483
+it('can delete a transaction with journal entries', function () {
484
+    $transaction = Transaction::factory()
485
+        ->forDefaultBankAccount()
486
+        ->forUncategorizedRevenue()
487
+        ->asDeposit(1000)
488
+        ->create();
489
+
490
+    expect($transaction->journalEntries()->count())->toBe(2);
491
+
492
+    livewire(Transactions::class)
493
+        ->callTableAction(DeleteAction::class, $transaction);
494
+
495
+    $this->assertModelMissing($transaction);
496
+
497
+    $this->assertDatabaseEmpty('journal_entries');
498
+});
499
+
500
+it('can bulk delete transactions with journal entries', function () {
501
+    $transactions = Transaction::factory()
502
+        ->forDefaultBankAccount()
503
+        ->forUncategorizedRevenue()
504
+        ->asDeposit(1000)
505
+        ->count(3)
506
+        ->create();
507
+
508
+    expect($transactions->count())->toBe(3);
509
+
510
+    livewire(Transactions::class)
511
+        ->callTableBulkAction(DeleteBulkAction::class, $transactions);
512
+
513
+    $this->assertDatabaseEmpty('transactions');
514
+    $this->assertDatabaseEmpty('journal_entries');
515
+});

+ 0
- 7
tests/Feature/ExampleTest.php 查看文件

@@ -1,7 +0,0 @@
1
-<?php
2
-
3
-it('returns a successful response', function () {
4
-    $response = $this->get('/');
5
-
6
-    $response->assertStatus(200);
7
-});

+ 16
- 0
tests/Helpers/helpers.php 查看文件

@@ -1,7 +1,10 @@
1 1
 <?php
2 2
 
3
+use App\Enums\Accounting\JournalEntryType;
3 4
 use App\Enums\Setting\EntityType;
4 5
 use App\Filament\Company\Pages\CreateCompany;
6
+use App\Models\Accounting\Account;
7
+use App\Models\Accounting\Transaction;
5 8
 use App\Models\Company;
6 9
 
7 10
 use function Pest\Livewire\livewire;
@@ -22,3 +25,16 @@ function createCompany(string $name): Company
22 25
 
23 26
     return auth()->user()->currentCompany;
24 27
 }
28
+
29
+/**
30
+ * Get the debit and credit accounts for a transaction.
31
+ *
32
+ * @return array<Account>
33
+ */
34
+function getTransactionDebitAndCreditAccounts(Transaction $transaction): array
35
+{
36
+    $debitAccount = $transaction->journalEntries->where('type', JournalEntryType::Debit)->firstOrFail()->account;
37
+    $creditAccount = $transaction->journalEntries->where('type', JournalEntryType::Credit)->firstOrFail()->account;
38
+
39
+    return [$debitAccount, $creditAccount];
40
+}

Loading…
取消
儲存