浏览代码

wip tests

3.x
Andrew Wallo 1年前
父节点
当前提交
58bf63bc32

+ 7
- 7
app/Filament/Company/Pages/Reports/TrialBalance.php 查看文件

34
 
34
 
35
     protected function initializeDefaultFilters(): void
35
     protected function initializeDefaultFilters(): void
36
     {
36
     {
37
-        if (empty($this->getFilterState('trialBalanceType'))) {
38
-            $this->setFilterState('trialBalanceType', 'regular');
37
+        if (empty($this->getFilterState('reportType'))) {
38
+            $this->setFilterState('reportType', 'standard');
39
         }
39
         }
40
     }
40
     }
41
 
41
 
63
         return $form
63
         return $form
64
             ->columns(4)
64
             ->columns(4)
65
             ->schema([
65
             ->schema([
66
-                Select::make('trialBalanceType')
67
-                    ->label('Trial Balance Type')
66
+                Select::make('reportType')
67
+                    ->label('Report Type')
68
                     ->options([
68
                     ->options([
69
-                        'regular' => 'Regular',
69
+                        'standard' => 'Standard',
70
                         'postClosing' => 'Post-Closing',
70
                         'postClosing' => 'Post-Closing',
71
                     ])
71
                     ])
72
                     ->selectablePlaceholder(false),
72
                     ->selectablePlaceholder(false),
73
                 DateRangeSelect::make('dateRange')
73
                 DateRangeSelect::make('dateRange')
74
-                    ->label('As of Date')
74
+                    ->label('As of')
75
                     ->selectablePlaceholder(false)
75
                     ->selectablePlaceholder(false)
76
                     ->endDateField('asOfDate'),
76
                     ->endDateField('asOfDate'),
77
                 $this->getAsOfDateFormComponent(),
77
                 $this->getAsOfDateFormComponent(),
80
 
80
 
81
     protected function buildReport(array $columns): ReportDTO
81
     protected function buildReport(array $columns): ReportDTO
82
     {
82
     {
83
-        return $this->reportService->buildTrialBalanceReport($this->getFilterState('trialBalanceType'), $this->getFormattedAsOfDate(), $columns);
83
+        return $this->reportService->buildTrialBalanceReport($this->getFilterState('reportType'), $this->getFormattedAsOfDate(), $columns);
84
     }
84
     }
85
 
85
 
86
     protected function getTransformer(ReportDTO $reportDTO): ExportableReport
86
     protected function getTransformer(ReportDTO $reportDTO): ExportableReport

+ 17
- 0
app/Models/User.php 查看文件

96
     {
96
     {
97
         return $this->hasMany(Department::class, 'manager_id');
97
         return $this->hasMany(Department::class, 'manager_id');
98
     }
98
     }
99
+
100
+    public function switchCompany(mixed $company): bool
101
+    {
102
+        if (! $this->belongsToCompany($company)) {
103
+            return false;
104
+        }
105
+
106
+        $this->forceFill([
107
+            'current_company_id' => $company->id,
108
+        ])->save();
109
+
110
+        $this->setRelation('currentCompany', $company);
111
+
112
+        session(['current_company_id' => $company->id]);
113
+
114
+        return true;
115
+    }
99
 }
116
 }

+ 3
- 3
app/Scopes/CurrentCompanyScope.php 查看文件

16
      */
16
      */
17
     public function apply(Builder $builder, Model $model): void
17
     public function apply(Builder $builder, Model $model): void
18
     {
18
     {
19
-        if (app()->runningInConsole()) {
19
+        $companyId = session('current_company_id');
20
+
21
+        if (! $companyId && app()->runningInConsole()) {
20
             return;
22
             return;
21
         }
23
         }
22
 
24
 
23
-        $companyId = session('current_company_id');
24
-
25
         if (! $companyId && Auth::check() && Auth::user()->currentCompany) {
25
         if (! $companyId && Auth::check() && Auth::user()->currentCompany) {
26
             $companyId = Auth::user()->currentCompany->id;
26
             $companyId = Auth::user()->currentCompany->id;
27
             session(['current_company_id' => $companyId]);
27
             session(['current_company_id' => $companyId]);

+ 36
- 22
app/Services/ReportService.php 查看文件

315
         return new ReportDTO($accountCategories, $formattedReportTotalBalances, $columns);
315
         return new ReportDTO($accountCategories, $formattedReportTotalBalances, $columns);
316
     }
316
     }
317
 
317
 
318
-    private function calculateTrialBalance(AccountCategory $category, int $endingBalance): array
318
+    public function calculateTrialBalance(AccountCategory $category, int $endingBalance): array
319
     {
319
     {
320
         if ($category->isNormalDebitBalance()) {
320
         if ($category->isNormalDebitBalance()) {
321
             if ($endingBalance >= 0) {
321
             if ($endingBalance >= 0) {
334
 
334
 
335
     public function buildIncomeStatementReport(string $startDate, string $endDate, array $columns = []): ReportDTO
335
     public function buildIncomeStatementReport(string $startDate, string $endDate, array $columns = []): ReportDTO
336
     {
336
     {
337
-        $accounts = $this->accountService->getAccountBalances($startDate, $endDate)->get();
337
+        // Query only relevant accounts and sort them at the query level
338
+        $revenueAccounts = $this->accountService->getAccountBalances($startDate, $endDate)
339
+            ->where('category', AccountCategory::Revenue)
340
+            ->orderByRaw('LENGTH(code), code')
341
+            ->get();
342
+
343
+        $cogsAccounts = $this->accountService->getAccountBalances($startDate, $endDate)
344
+            ->whereRelation('subtype', 'name', 'Cost of Goods Sold')
345
+            ->orderByRaw('LENGTH(code), code')
346
+            ->get();
347
+
348
+        $expenseAccounts = $this->accountService->getAccountBalances($startDate, $endDate)
349
+            ->where('category', AccountCategory::Expense)
350
+            ->whereRelation('subtype', 'name', '!=', 'Cost of Goods Sold')
351
+            ->orderByRaw('LENGTH(code), code')
352
+            ->get();
338
 
353
 
339
         $accountCategories = [];
354
         $accountCategories = [];
340
         $totalRevenue = 0;
355
         $totalRevenue = 0;
341
-        $cogs = 0;
356
+        $totalCogs = 0;
342
         $totalExpenses = 0;
357
         $totalExpenses = 0;
343
 
358
 
359
+        // Define category groups
344
         $categoryGroups = [
360
         $categoryGroups = [
345
-            'Revenue' => [
346
-                'accounts' => $accounts->where('category', AccountCategory::Revenue),
361
+            AccountCategory::Revenue->getPluralLabel() => [
362
+                'accounts' => $revenueAccounts,
347
                 'total' => &$totalRevenue,
363
                 'total' => &$totalRevenue,
348
             ],
364
             ],
349
             'Cost of Goods Sold' => [
365
             'Cost of Goods Sold' => [
350
-                'accounts' => $accounts->where('subtype.name', 'Cost of Goods Sold'),
351
-                'total' => &$cogs,
366
+                'accounts' => $cogsAccounts,
367
+                'total' => &$totalCogs,
352
             ],
368
             ],
353
-            'Expenses' => [
354
-                'accounts' => $accounts->where('category', AccountCategory::Expense)->where('subtype.name', '!=', 'Cost of Goods Sold'),
369
+            AccountCategory::Expense->getPluralLabel() => [
370
+                'accounts' => $expenseAccounts,
355
                 'total' => &$totalExpenses,
371
                 'total' => &$totalExpenses,
356
             ],
372
             ],
357
         ];
373
         ];
358
 
374
 
375
+        // Process each category group
359
         foreach ($categoryGroups as $label => $group) {
376
         foreach ($categoryGroups as $label => $group) {
360
             $categoryAccounts = [];
377
             $categoryAccounts = [];
361
             $netMovement = 0;
378
             $netMovement = 0;
362
 
379
 
363
-            foreach ($group['accounts']->sortBy('code', SORT_NATURAL) as $account) {
364
-                $category = null;
365
-
366
-                if ($label === 'Revenue') {
367
-                    $category = AccountCategory::Revenue;
368
-                } elseif ($label === 'Expenses') {
369
-                    $category = AccountCategory::Expense;
370
-                } elseif ($label === 'Cost of Goods Sold') {
371
-                    // COGS is treated as part of Expenses, so we use AccountCategory::Expense
372
-                    $category = AccountCategory::Expense;
373
-                }
380
+            foreach ($group['accounts'] as $account) {
381
+                // Use the category type based on label
382
+                $category = match ($label) {
383
+                    AccountCategory::Revenue->getPluralLabel() => AccountCategory::Revenue,
384
+                    AccountCategory::Expense->getPluralLabel(), 'Cost of Goods Sold' => AccountCategory::Expense,
385
+                    default => null
386
+                };
374
 
387
 
375
                 if ($category !== null) {
388
                 if ($category !== null) {
376
                     $accountBalances = $this->calculateAccountBalances($account, $category);
389
                     $accountBalances = $this->calculateAccountBalances($account, $category);
391
 
404
 
392
             $accountCategories[$label] = new AccountCategoryDTO(
405
             $accountCategories[$label] = new AccountCategoryDTO(
393
                 $categoryAccounts,
406
                 $categoryAccounts,
394
-                $this->formatBalances(['net_movement' => $netMovement]),
407
+                $this->formatBalances(['net_movement' => $netMovement])
395
             );
408
             );
396
         }
409
         }
397
 
410
 
398
-        $grossProfit = $totalRevenue - $cogs;
411
+        // Calculate gross and net profit
412
+        $grossProfit = $totalRevenue - $totalCogs;
399
         $netProfit = $grossProfit - $totalExpenses;
413
         $netProfit = $grossProfit - $totalExpenses;
400
         $formattedReportTotalBalances = $this->formatBalances(['net_movement' => $netProfit]);
414
         $formattedReportTotalBalances = $this->formatBalances(['net_movement' => $netProfit]);
401
 
415
 

+ 2
- 1
composer.json 查看文件

34
         "laravel/sail": "^1.26",
34
         "laravel/sail": "^1.26",
35
         "mockery/mockery": "^1.6",
35
         "mockery/mockery": "^1.6",
36
         "nunomaduro/collision": "^8.0",
36
         "nunomaduro/collision": "^8.0",
37
-        "phpunit/phpunit": "^10.5",
37
+        "pestphp/pest": "^3.0",
38
+        "pestphp/pest-plugin-livewire": "^3.0",
38
         "spatie/laravel-ignition": "^2.4",
39
         "spatie/laravel-ignition": "^2.4",
39
         "spatie/laravel-ray": "^1.36"
40
         "spatie/laravel-ray": "^1.36"
40
     },
41
     },

+ 1257
- 365
composer.lock
文件差异内容过多而无法显示
查看文件


+ 9
- 1
database/factories/Accounting/TransactionFactory.php 查看文件

93
                 ->where('company_id', $company->id)
93
                 ->where('company_id', $company->id)
94
                 ->where('id', '<>', $accountIdForBankAccount)
94
                 ->where('id', '<>', $accountIdForBankAccount)
95
                 ->inRandomOrder()
95
                 ->inRandomOrder()
96
-                ->firstOrFail();
96
+                ->first();
97
+
98
+            // If no matching account is found, use a fallback
99
+            if (! $account) {
100
+                $account = Account::where('company_id', $company->id)
101
+                    ->where('id', '<>', $accountIdForBankAccount)
102
+                    ->inRandomOrder()
103
+                    ->firstOrFail(); // Ensure there is at least some account
104
+            }
97
 
105
 
98
             return [
106
             return [
99
                 'company_id' => $company->id,
107
                 'company_id' => $company->id,

+ 40
- 0
database/factories/CompanyFactory.php 查看文件

3
 namespace Database\Factories;
3
 namespace Database\Factories;
4
 
4
 
5
 use App\Models\Company;
5
 use App\Models\Company;
6
+use App\Models\Setting\CompanyProfile;
6
 use App\Models\User;
7
 use App\Models\User;
8
+use App\Services\CompanyDefaultService;
9
+use Database\Factories\Accounting\TransactionFactory;
7
 use Illuminate\Database\Eloquent\Factories\Factory;
10
 use Illuminate\Database\Eloquent\Factories\Factory;
11
+use Illuminate\Support\Facades\DB;
8
 
12
 
9
 class CompanyFactory extends Factory
13
 class CompanyFactory extends Factory
10
 {
14
 {
28
             'personal_company' => true,
32
             'personal_company' => true,
29
         ];
33
         ];
30
     }
34
     }
35
+
36
+    public function withCompanyProfile(): self
37
+    {
38
+        return $this->afterCreating(function (Company $company) {
39
+            CompanyProfile::factory()->forCompany($company)->create();
40
+        });
41
+    }
42
+
43
+    /**
44
+     * Set up default settings for the company after creation.
45
+     */
46
+    public function withCompanyDefaults(): self
47
+    {
48
+        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
+            });
54
+        });
55
+    }
56
+
57
+    public function withTransactions(int $count = 2000): self
58
+    {
59
+        return $this->afterCreating(function (Company $company) use ($count) {
60
+            $defaultBankAccount = $company->default->bankAccount;
61
+
62
+            TransactionFactory::new()
63
+                ->forCompanyAndBankAccount($company, $defaultBankAccount)
64
+                ->count($count)
65
+                ->createQuietly([
66
+                    'created_by' => $company->user_id,
67
+                    'updated_by' => $company->user_id,
68
+                ]);
69
+        });
70
+    }
31
 }
71
 }

+ 18
- 10
database/factories/Setting/CompanyProfileFactory.php 查看文件

3
 namespace Database\Factories\Setting;
3
 namespace Database\Factories\Setting;
4
 
4
 
5
 use App\Enums\Setting\EntityType;
5
 use App\Enums\Setting\EntityType;
6
-use App\Faker\PhoneNumber;
7
 use App\Faker\State;
6
 use App\Faker\State;
7
+use App\Models\Company;
8
 use App\Models\Setting\CompanyProfile;
8
 use App\Models\Setting\CompanyProfile;
9
 use Illuminate\Database\Eloquent\Factories\Factory;
9
 use Illuminate\Database\Eloquent\Factories\Factory;
10
 
10
 
25
      */
25
      */
26
     public function definition(): array
26
     public function definition(): array
27
     {
27
     {
28
+        $countryCode = $this->faker->countryCode;
29
+
28
         return [
30
         return [
29
             'address' => $this->faker->streetAddress,
31
             'address' => $this->faker->streetAddress,
30
             'zip_code' => $this->faker->postcode,
32
             'zip_code' => $this->faker->postcode,
33
+            'state_id' => $this->faker->state($countryCode),
34
+            'country' => $countryCode,
35
+            'phone_number' => $this->faker->phoneNumberForCountryCode($countryCode),
31
             'email' => $this->faker->email,
36
             'email' => $this->faker->email,
32
             'entity_type' => $this->faker->randomElement(EntityType::class),
37
             'entity_type' => $this->faker->randomElement(EntityType::class),
33
         ];
38
         ];
34
     }
39
     }
35
 
40
 
36
-    public function withCountry(string $code): static
41
+    public function withCountry(string $code): self
37
     {
42
     {
38
-        /** @var PhoneNumber $phoneFaker */
39
-        $phoneFaker = $this->faker;
40
-
41
-        /** @var State $stateFaker */
42
-        $stateFaker = $this->faker;
43
-
44
         return $this->state([
43
         return $this->state([
45
             'country' => $code,
44
             'country' => $code,
46
-            'state_id' => $stateFaker->state($code),
47
-            'phone_number' => $phoneFaker->phoneNumberForCountryCode($code),
45
+            'state_id' => $this->faker->state($code),
46
+            'phone_number' => $this->faker->phoneNumberForCountryCode($code),
47
+        ]);
48
+    }
49
+
50
+    public function forCompany(Company $company): self
51
+    {
52
+        return $this->state([
53
+            'company_id' => $company->id,
54
+            'created_by' => $company->owner->id,
55
+            'updated_by' => $company->owner->id,
48
         ]);
56
         ]);
49
     }
57
     }
50
 }
58
 }

+ 9
- 36
database/factories/UserFactory.php 查看文件

3
 namespace Database\Factories;
3
 namespace Database\Factories;
4
 
4
 
5
 use App\Models\Company;
5
 use App\Models\Company;
6
-use App\Models\Setting\CompanyProfile;
7
 use App\Models\User;
6
 use App\Models\User;
8
-use App\Services\CompanyDefaultService;
9
-use Database\Factories\Accounting\TransactionFactory;
10
 use Illuminate\Database\Eloquent\Factories\Factory;
7
 use Illuminate\Database\Eloquent\Factories\Factory;
11
-use Illuminate\Support\Facades\DB;
12
 use Illuminate\Support\Facades\Hash;
8
 use Illuminate\Support\Facades\Hash;
13
 use Illuminate\Support\Str;
9
 use Illuminate\Support\Str;
14
 use Wallo\FilamentCompanies\FilamentCompanies;
10
 use Wallo\FilamentCompanies\FilamentCompanies;
58
     /**
54
     /**
59
      * Indicate that the user should have a personal company.
55
      * Indicate that the user should have a personal company.
60
      */
56
      */
61
-    public function withPersonalCompany(): static
57
+    public function withPersonalCompany(?callable $callback = null): static
62
     {
58
     {
63
         if (! FilamentCompanies::hasCompanyFeatures()) {
59
         if (! FilamentCompanies::hasCompanyFeatures()) {
64
             return $this->state([]);
60
             return $this->state([]);
65
         }
61
         }
66
 
62
 
67
-        $countryCode = $this->faker->countryCode;
68
-
69
-        return $this->afterCreating(function (User $user) use ($countryCode) {
63
+        return $this->has(
70
             Company::factory()
64
             Company::factory()
71
-                ->has(
72
-                    CompanyProfile::factory()
73
-                        ->withCountry($countryCode)
74
-                        ->state([
75
-                            'created_by' => $user->id,
76
-                            'updated_by' => $user->id,
77
-                        ]),
78
-                    'profile'
79
-                )
80
-                ->afterCreating(function (Company $company) use ($user, $countryCode) {
81
-                    DB::transaction(function () use ($company, $user, $countryCode) {
82
-                        $companyDefaultService = app()->make(CompanyDefaultService::class);
83
-                        $companyDefaultService->createCompanyDefaults($company, $user, 'USD', $countryCode, 'en');
84
-
85
-                        $defaultBankAccount = $company->default->bankAccount;
86
-
87
-                        TransactionFactory::new()
88
-                            ->forCompanyAndBankAccount($company, $defaultBankAccount)
89
-                            ->count(2000)
90
-                            ->createQuietly([
91
-                                'created_by' => $user->id,
92
-                                'updated_by' => $user->id,
93
-                            ]);
94
-                    });
95
-                })
96
-                ->create([
65
+                ->withCompanyProfile()
66
+                ->withCompanyDefaults()
67
+                ->state(fn (array $attributes, User $user) => [
97
                     'name' => $user->name . '\'s Company',
68
                     'name' => $user->name . '\'s Company',
98
                     'user_id' => $user->id,
69
                     'user_id' => $user->id,
99
                     'personal_company' => true,
70
                     'personal_company' => true,
100
-                ]);
101
-        });
71
+                ])
72
+                ->when(is_callable($callback), $callback),
73
+            'ownedCompanies'
74
+        );
102
     }
75
     }
103
 }
76
 }

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

3
 namespace Database\Seeders;
3
 namespace Database\Seeders;
4
 
4
 
5
 use App\Models\User;
5
 use App\Models\User;
6
+use Database\Factories\CompanyFactory;
6
 use Illuminate\Database\Seeder;
7
 use Illuminate\Database\Seeder;
7
 
8
 
8
 class DatabaseSeeder extends Seeder
9
 class DatabaseSeeder extends Seeder
14
     {
15
     {
15
         // Create a single admin user and their personal company
16
         // Create a single admin user and their personal company
16
         $adminUser = User::factory()
17
         $adminUser = User::factory()
17
-            ->withPersonalCompany()  // Ensures the user has a personal company created alongside
18
+            ->withPersonalCompany(function (CompanyFactory $factory) {
19
+                return $factory->withTransactions();
20
+            })
18
             ->create([
21
             ->create([
19
                 'name' => 'Admin',
22
                 'name' => 'Admin',
20
                 'email' => 'admin@gmail.com',
23
                 'email' => 'admin@gmail.com',

+ 21
- 0
database/seeders/TestDatabaseSeeder.php 查看文件

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

+ 2
- 2
phpunit.xml 查看文件

22
         <env name="APP_MAINTENANCE_DRIVER" value="file"/>
22
         <env name="APP_MAINTENANCE_DRIVER" value="file"/>
23
         <env name="BCRYPT_ROUNDS" value="4"/>
23
         <env name="BCRYPT_ROUNDS" value="4"/>
24
         <env name="CACHE_STORE" value="array"/>
24
         <env name="CACHE_STORE" value="array"/>
25
-        <!-- <env name="DB_CONNECTION" value="sqlite"/> -->
26
-        <!-- <env name="DB_DATABASE" value=":memory:"/> -->
25
+        <env name="DB_CONNECTION" value="mysql"/>
26
+        <env name="DB_DATABASE" value="erpsaas_test"/>
27
         <env name="MAIL_MAILER" value="array"/>
27
         <env name="MAIL_MAILER" value="array"/>
28
         <env name="PULSE_ENABLED" value="false"/>
28
         <env name="PULSE_ENABLED" value="false"/>
29
         <env name="QUEUE_CONNECTION" value="sync"/>
29
         <env name="QUEUE_CONNECTION" value="sync"/>

+ 64
- 0
tests/Feature/CompanySetupAndBehaviorTest.php 查看文件

1
+<?php
2
+
3
+use App\Models\Accounting\Transaction;
4
+
5
+it('initially assigns a personal company to the test user', function () {
6
+    $testUser = $this->testUser;
7
+    $testCompany = $this->testCompany;
8
+
9
+    expect($testUser)->not->toBeNull()
10
+        ->and($testCompany)->not->toBeNull()
11
+        ->and($testCompany->personal_company)->toBeTrue()
12
+        ->and($testUser->currentCompany->id)->toBe($testCompany->id);
13
+});
14
+
15
+it('can create a new company and switches to it automatically', function () {
16
+    $testUser = $this->testUser;
17
+    $testCompany = $this->testCompany;
18
+
19
+    $newCompany = createCompany('New Company');
20
+
21
+    expect($newCompany)->not->toBeNull()
22
+        ->and($newCompany->name)->toBe('New Company')
23
+        ->and($newCompany->personal_company)->toBeFalse()
24
+        ->and($testUser->currentCompany->id)->toBe($newCompany->id)
25
+        ->and($newCompany->id)->not->toBe($testCompany->id);
26
+});
27
+
28
+it('returns data for the current company based on the CurrentCompanyScope', function () {
29
+    $testUser = $this->testUser;
30
+    $testCompany = $this->testCompany;
31
+
32
+    Transaction::factory()
33
+        ->forCompanyAndBankAccount($testCompany, $testCompany->default->bankAccount)
34
+        ->count(10)
35
+        ->create();
36
+
37
+    $newCompany = createCompany('New Company');
38
+
39
+    expect($testUser->currentCompany->id)
40
+        ->toBe($newCompany->id)
41
+        ->not->toBe($testCompany->id);
42
+
43
+    Transaction::factory()
44
+        ->forCompanyAndBankAccount($newCompany, $newCompany->default->bankAccount)
45
+        ->count(5)
46
+        ->create();
47
+
48
+    expect(Transaction::count())->toBe(5);
49
+
50
+    $testUser->switchCompany($testCompany);
51
+
52
+    expect($testUser->currentCompany->id)->toBe($testCompany->id)
53
+        ->and(Transaction::count())->toBe(10);
54
+});
55
+
56
+it('validates that company default settings are non-null', function () {
57
+    $testCompany = $this->testCompany;
58
+
59
+    expect($testCompany->profile->country)->not->toBeNull()
60
+        ->and($testCompany->profile->email)->not->toBeNull()
61
+        ->and($testCompany->default->currency_code)->toBe('USD')
62
+        ->and($testCompany->locale->language)->toBe('en')
63
+        ->and($testCompany->default->bankAccount->account->name)->toBe('Cash on Hand');
64
+});

+ 4
- 16
tests/Feature/ExampleTest.php 查看文件

1
 <?php
1
 <?php
2
 
2
 
3
-namespace Tests\Feature;
3
+it('returns a successful response', function () {
4
+    $response = $this->get('/');
4
 
5
 
5
-// use Illuminate\Foundation\Testing\RefreshDatabase;
6
-use Tests\TestCase;
7
-
8
-class ExampleTest extends TestCase
9
-{
10
-    /**
11
-     * A basic test example.
12
-     */
13
-    public function test_the_application_returns_a_successful_response(): void
14
-    {
15
-        $response = $this->get('/');
16
-
17
-        $response->assertStatus(200);
18
-    }
19
-}
6
+    $response->assertStatus(200);
7
+});

+ 237
- 0
tests/Feature/Reports/TrialBalanceReportTest.php 查看文件

1
+<?php
2
+
3
+use App\Contracts\ExportableReport;
4
+use App\Enums\Accounting\AccountCategory;
5
+use App\Filament\Company\Pages\Reports\TrialBalance;
6
+use App\Models\Accounting\Transaction;
7
+use App\Services\AccountService;
8
+use App\Services\ReportService;
9
+use Filament\Facades\Filament;
10
+use Illuminate\Support\Carbon;
11
+
12
+use function Pest\Livewire\livewire;
13
+
14
+it('correctly builds a standard trial balance report', function () {
15
+    $testUser = $this->testUser;
16
+    $testCompany = $this->testCompany;
17
+    $defaultBankAccount = $testCompany->default->bankAccount;
18
+
19
+    $fiscalYearStartDate = $testCompany->locale->fiscalYearStartDate();
20
+    $fiscalYearEndDate = $testCompany->locale->fiscalYearEndDate();
21
+    $defaultEndDate = Carbon::parse($fiscalYearEndDate);
22
+    $defaultAsOfDate = $defaultEndDate->isFuture() ? now()->endOfDay() : $defaultEndDate->endOfDay();
23
+
24
+    $defaultDateRange = 'FY-' . now()->year;
25
+    $defaultReportType = 'standard';
26
+
27
+    // Create transactions for the company
28
+    Transaction::factory()
29
+        ->forCompanyAndBankAccount($testCompany, $defaultBankAccount)
30
+        ->count(10)
31
+        ->create();
32
+
33
+    // Instantiate services
34
+    $accountService = app(AccountService::class);
35
+    $reportService = app(ReportService::class);
36
+
37
+    // Calculate balances
38
+    $asOfStartDate = $accountService->getEarliestTransactionDate();
39
+    $netMovement = $accountService->getNetMovement($defaultBankAccount->account, $asOfStartDate, $defaultAsOfDate);
40
+    $netMovementAmount = $netMovement->getAmount();
41
+
42
+    // Verify trial balance calculations
43
+    $trialBalance = $reportService->calculateTrialBalance($defaultBankAccount->account->category, $netMovementAmount);
44
+    expect($trialBalance)->toMatchArray([
45
+        'debit_balance' => max($netMovementAmount, 0),
46
+        'credit_balance' => $netMovementAmount < 0 ? abs($netMovementAmount) : 0,
47
+    ]);
48
+
49
+    $formattedBalances = $reportService->formatBalances($trialBalance);
50
+
51
+    $accountCategoryPluralLabels = array_map(fn ($category) => $category->getPluralLabel(), AccountCategory::getOrderedCategories());
52
+
53
+    Filament::setTenant($testCompany);
54
+
55
+    $component = livewire(TrialBalance::class)
56
+        ->assertFormSet([
57
+            'deferredFilters.reportType' => $defaultReportType,
58
+            'deferredFilters.dateRange' => $defaultDateRange,
59
+            'deferredFilters.asOfDate' => $defaultAsOfDate->toDateTimeString(),
60
+        ])
61
+        ->assertSet('filters', [
62
+            'reportType' => $defaultReportType,
63
+            'dateRange' => $defaultDateRange,
64
+            'asOfDate' => $defaultAsOfDate->toDateString(),
65
+        ])
66
+        ->call('applyFilters')
67
+        ->assertSeeTextInOrder($accountCategoryPluralLabels)
68
+        ->assertDontSeeText('Retained Earnings')
69
+        ->assertSeeTextInOrder([
70
+            $formattedBalances->debitBalance,
71
+            $formattedBalances->creditBalance,
72
+        ]);
73
+
74
+    /** @var ExportableReport $report */
75
+    $report = $component->report;
76
+
77
+    $columnLabels = array_map(static fn ($column) => $column->getLabel(), $report->getColumns());
78
+
79
+    $component->assertSeeTextInOrder($columnLabels);
80
+
81
+    $categories = $report->getCategories();
82
+
83
+    foreach ($categories as $category) {
84
+        $header = $category->header;
85
+        $data = $category->data;
86
+        $summary = $category->summary;
87
+
88
+        $component->assertSeeTextInOrder($header);
89
+
90
+        foreach ($data as $row) {
91
+            $flatRow = [];
92
+
93
+            foreach ($row as $value) {
94
+                if (is_array($value)) {
95
+                    $flatRow[] = $value['name'];
96
+                } else {
97
+                    $flatRow[] = $value;
98
+                }
99
+            }
100
+
101
+            $component->assertSeeTextInOrder($flatRow);
102
+        }
103
+
104
+        $component->assertSeeTextInOrder($summary);
105
+    }
106
+
107
+    $overallTotals = $report->getOverallTotals();
108
+    $component->assertSeeTextInOrder($overallTotals);
109
+});
110
+
111
+it('correctly builds a post-closing trial balance report', function () {
112
+    $testUser = $this->testUser;
113
+    $testCompany = $this->testCompany;
114
+    $defaultBankAccount = $testCompany->default->bankAccount;
115
+
116
+    $fiscalYearStartDate = $testCompany->locale->fiscalYearStartDate();
117
+    $fiscalYearEndDate = $testCompany->locale->fiscalYearEndDate();
118
+    $defaultEndDate = Carbon::parse($fiscalYearEndDate);
119
+    $defaultAsOfDate = $defaultEndDate->isFuture() ? now()->endOfDay() : $defaultEndDate->endOfDay();
120
+
121
+    $defaultDateRange = 'FY-' . now()->year;
122
+    $defaultReportType = 'postClosing';
123
+
124
+    // Create transactions for the company
125
+    Transaction::factory()
126
+        ->forCompanyAndBankAccount($testCompany, $defaultBankAccount)
127
+        ->count(10)
128
+        ->create();
129
+
130
+    // Instantiate services
131
+    $accountService = app(AccountService::class);
132
+    $reportService = app(ReportService::class);
133
+
134
+    // Calculate balances
135
+    $asOfStartDate = $accountService->getEarliestTransactionDate();
136
+    $netMovement = $accountService->getNetMovement($defaultBankAccount->account, $asOfStartDate, $defaultAsOfDate);
137
+    $netMovementAmount = $netMovement->getAmount();
138
+
139
+    // Verify trial balance calculations
140
+    $trialBalance = $reportService->calculateTrialBalance($defaultBankAccount->account->category, $netMovementAmount);
141
+    expect($trialBalance)->toMatchArray([
142
+        'debit_balance' => max($netMovementAmount, 0),
143
+        'credit_balance' => $netMovementAmount < 0 ? abs($netMovementAmount) : 0,
144
+    ]);
145
+
146
+    $formattedBalances = $reportService->formatBalances($trialBalance);
147
+
148
+    $accountCategoryPluralLabels = array_map(fn ($category) => $category->getPluralLabel(), AccountCategory::getOrderedCategories());
149
+
150
+    $retainedEarningsAmount = $reportService->calculateRetainedEarnings($asOfStartDate, $defaultAsOfDate)->getAmount();
151
+
152
+    $isCredit = $retainedEarningsAmount >= 0;
153
+
154
+    $retainedEarningsDebitAmount = $isCredit ? 0 : abs($retainedEarningsAmount);
155
+    $retainedEarningsCreditAmount = $isCredit ? $retainedEarningsAmount : 0;
156
+
157
+    $formattedRetainedEarnings = $reportService->formatBalances([
158
+        'debit_balance' => $retainedEarningsDebitAmount,
159
+        'credit_balance' => $retainedEarningsCreditAmount,
160
+    ]);
161
+
162
+    $retainedEarningsRow = [
163
+        'RE',
164
+        'Retained Earnings',
165
+        $formattedRetainedEarnings->debitBalance,
166
+        $formattedRetainedEarnings->creditBalance,
167
+    ];
168
+
169
+    Filament::setTenant($testCompany);
170
+
171
+    $component = livewire(TrialBalance::class)
172
+        ->set('deferredFilters.reportType', $defaultReportType)
173
+        ->call('applyFilters')
174
+        ->assertFormSet([
175
+            'deferredFilters.reportType' => $defaultReportType,
176
+            'deferredFilters.dateRange' => $defaultDateRange,
177
+            'deferredFilters.asOfDate' => $defaultAsOfDate->toDateTimeString(),
178
+        ])
179
+        ->assertSet('filters', [
180
+            'reportType' => $defaultReportType,
181
+            'dateRange' => $defaultDateRange,
182
+            'asOfDate' => $defaultAsOfDate->toDateString(),
183
+        ])
184
+        ->call('applyFilters')
185
+        ->assertSeeTextInOrder($retainedEarningsRow)
186
+        ->assertSeeTextInOrder([
187
+            'Total Revenue',
188
+            '$0.00',
189
+            '$0.00',
190
+        ])
191
+        ->assertSeeTextInOrder([
192
+            'Total Expenses',
193
+            '$0.00',
194
+            '$0.00',
195
+        ])
196
+        ->assertSeeTextInOrder($accountCategoryPluralLabels)
197
+        ->assertSeeTextInOrder([
198
+            $formattedBalances->debitBalance,
199
+            $formattedBalances->creditBalance,
200
+        ]);
201
+
202
+    /** @var ExportableReport $report */
203
+    $report = $component->report;
204
+
205
+    $columnLabels = array_map(static fn ($column) => $column->getLabel(), $report->getColumns());
206
+
207
+    $component->assertSeeTextInOrder($columnLabels);
208
+
209
+    $categories = $report->getCategories();
210
+
211
+    foreach ($categories as $category) {
212
+        $header = $category->header;
213
+        $data = $category->data;
214
+        $summary = $category->summary;
215
+
216
+        $component->assertSeeTextInOrder($header);
217
+
218
+        foreach ($data as $row) {
219
+            $flatRow = [];
220
+
221
+            foreach ($row as $value) {
222
+                if (is_array($value)) {
223
+                    $flatRow[] = $value['name'];
224
+                } else {
225
+                    $flatRow[] = $value;
226
+                }
227
+            }
228
+
229
+            $component->assertSeeTextInOrder($flatRow);
230
+        }
231
+
232
+        $component->assertSeeTextInOrder($summary);
233
+    }
234
+
235
+    $overallTotals = $report->getOverallTotals();
236
+    $component->assertSeeTextInOrder($overallTotals);
237
+});

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

1
+<?php
2
+
3
+use App\Enums\Setting\EntityType;
4
+use App\Filament\Company\Pages\CreateCompany;
5
+use App\Models\Company;
6
+
7
+use function Pest\Livewire\livewire;
8
+
9
+function createCompany(string $name): Company
10
+{
11
+    livewire(CreateCompany::class)
12
+        ->fillForm([
13
+            'name' => $name,
14
+            'profile.email' => 'company@gmail.com',
15
+            'profile.entity_type' => EntityType::LimitedLiabilityCompany,
16
+            'profile.country' => 'US',
17
+            'locale.language' => 'en',
18
+            'currencies.code' => 'USD',
19
+        ])
20
+        ->call('register')
21
+        ->assertHasNoErrors();
22
+
23
+    return auth()->user()->currentCompany;
24
+}

+ 35
- 0
tests/Pest.php 查看文件

1
+<?php
2
+
3
+uses(Tests\TestCase::class)
4
+    ->in('Feature', 'Unit');
5
+
6
+/*
7
+|--------------------------------------------------------------------------
8
+| Expectations
9
+|--------------------------------------------------------------------------
10
+|
11
+| When you're writing tests, you often need to check that values meet certain conditions. The
12
+| "expect()" function gives you access to a set of "expectations" methods that you can use
13
+| to assert different things. Of course, you may extend the Expectation API at any time.
14
+|
15
+*/
16
+
17
+expect()->extend('toBeOne', function () {
18
+    return $this->toBe(1);
19
+});
20
+
21
+/*
22
+|--------------------------------------------------------------------------
23
+| Functions
24
+|--------------------------------------------------------------------------
25
+|
26
+| While Pest is very powerful out-of-the-box, you may have some testing code specific to your
27
+| project that you don't want to repeat in every file. Here you can also expose helpers as
28
+| global functions to help you to reduce the number of lines of code in your test files.
29
+|
30
+*/
31
+
32
+function something()
33
+{
34
+    // ..
35
+}

+ 33
- 1
tests/TestCase.php 查看文件

2
 
2
 
3
 namespace Tests;
3
 namespace Tests;
4
 
4
 
5
+use App\Models\Company;
6
+use App\Models\User;
7
+use Database\Seeders\TestDatabaseSeeder;
8
+use Illuminate\Foundation\Testing\RefreshDatabase;
5
 use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
9
 use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
6
 
10
 
7
 abstract class TestCase extends BaseTestCase
11
 abstract class TestCase extends BaseTestCase
8
 {
12
 {
9
-    //
13
+    use RefreshDatabase;
14
+
15
+    /**
16
+     * Indicates whether the default seeder should run before each test.
17
+     */
18
+    protected bool $seed = true;
19
+
20
+    /**
21
+     * Run a specific seeder before each test.
22
+     */
23
+    protected string $seeder = TestDatabaseSeeder::class;
24
+
25
+    protected User $testUser;
26
+
27
+    protected ?Company $testCompany;
28
+
29
+    protected function setUp(): void
30
+    {
31
+        parent::setUp();
32
+
33
+        $this->testUser = User::first();
34
+
35
+        $this->testCompany = $this->testUser->ownedCompanies->first();
36
+
37
+        $this->testUser->switchCompany($this->testCompany);
38
+
39
+        $this->actingAs($this->testUser)
40
+            ->withSession(['current_company_id' => $this->testCompany->id]);
41
+    }
10
 }
42
 }

+ 3
- 14
tests/Unit/ExampleTest.php 查看文件

1
 <?php
1
 <?php
2
 
2
 
3
-namespace Tests\Unit;
4
-
5
-use PHPUnit\Framework\TestCase;
6
-
7
-class ExampleTest extends TestCase
8
-{
9
-    /**
10
-     * A basic test example.
11
-     */
12
-    public function test_that_true_is_true(): void
13
-    {
14
-        $this->assertTrue(true);
15
-    }
16
-}
3
+test('true is true', function () {
4
+    expect(true)->toBeTrue();
5
+});

正在加载...
取消
保存