浏览代码

Trial Balance report and refactor

3.x
wallo 1年前
父节点
当前提交
0175e82eb9
共有 33 个文件被更改,包括 567 次插入477 次删除
  1. 0
    9
      app/Contracts/AccountHandler.php
  2. 1
    1
      app/DTO/ReportDTO.php
  3. 3
    8
      app/Events/CompanyConfigured.php
  4. 3
    5
      app/Events/CompanyDefaultEvent.php
  5. 4
    8
      app/Events/CompanyDefaultUpdated.php
  6. 7
    17
      app/Events/CompanyGenerated.php
  7. 5
    14
      app/Events/CurrencyRateChanged.php
  8. 3
    5
      app/Events/DefaultCurrencyChanged.php
  9. 5
    11
      app/Events/PlaidSuccess.php
  10. 6
    14
      app/Events/StartTransactionImport.php
  11. 0
    6
      app/Facades/Accounting.php
  12. 2
    1
      app/Filament/Company/Pages/Reports.php
  13. 10
    80
      app/Filament/Company/Pages/Reports/AccountBalances.php
  14. 77
    0
      app/Filament/Company/Pages/Reports/BaseReportPage.php
  15. 34
    0
      app/Filament/Company/Pages/Reports/TrialBalance.php
  16. 4
    4
      app/Http/Controllers/PlaidWebhookController.php
  17. 7
    20
      app/Jobs/ProcessTransactionImport.php
  18. 5
    12
      app/Jobs/ProcessTransactionUpdate.php
  19. 5
    7
      app/Listeners/CreateConnectedAccount.php
  20. 3
    5
      app/Listeners/HandleTransactionImport.php
  21. 3
    3
      app/Listeners/UpdateCurrencyRates.php
  22. 2
    2
      app/Services/AccountBalancesExportService.php
  23. 7
    99
      app/Services/AccountService.php
  24. 4
    8
      app/Services/ConnectedBankAccountService.php
  25. 8
    14
      app/Services/CurrencyService.php
  26. 47
    37
      app/Services/PlaidService.php
  27. 170
    0
      app/Services/ReportService.php
  28. 4
    8
      app/ValueObjects/Money.php
  29. 4
    8
      app/View/Models/InvoiceViewModel.php
  30. 53
    54
      composer.lock
  31. 16
    16
      package-lock.json
  32. 1
    1
      resources/views/filament/company/pages/reports/account-balances.blade.php
  33. 64
    0
      resources/views/filament/company/pages/reports/trial-balance.blade.php

+ 0
- 9
app/Contracts/AccountHandler.php 查看文件

@@ -2,9 +2,6 @@
2 2
 
3 3
 namespace App\Contracts;
4 4
 
5
-use App\DTO\AccountBalanceDTO;
6
-use App\DTO\AccountBalanceReportDTO;
7
-use App\Enums\Accounting\AccountCategory;
8 5
 use App\Models\Accounting\Account;
9 6
 use App\ValueObjects\Money;
10 7
 
@@ -20,14 +17,8 @@ interface AccountHandler
20 17
 
21 18
     public function getEndingBalance(Account $account, string $startDate, string $endDate): ?Money;
22 19
 
23
-    public function calculateNetMovementByCategory(AccountCategory $category, int $debitBalance, int $creditBalance): int;
24
-
25 20
     public function getBalances(Account $account, string $startDate, string $endDate): array;
26 21
 
27
-    public function formatBalances(array $balances): AccountBalanceDTO;
28
-
29
-    public function buildAccountBalanceReport(string $startDate, string $endDate): AccountBalanceReportDTO;
30
-
31 22
     public function getTotalBalanceForAllBankAccounts(string $startDate, string $endDate): Money;
32 23
 
33 24
     public function getAccountCategoryOrder(): array;

app/DTO/AccountBalanceReportDTO.php → app/DTO/ReportDTO.php 查看文件

@@ -4,7 +4,7 @@ namespace App\DTO;
4 4
 
5 5
 use Livewire\Wireable;
6 6
 
7
-class AccountBalanceReportDTO implements Wireable
7
+class ReportDTO implements Wireable
8 8
 {
9 9
     public function __construct(
10 10
         /**

+ 3
- 8
app/Events/CompanyConfigured.php 查看文件

@@ -13,13 +13,8 @@ class CompanyConfigured
13 13
     use InteractsWithSockets;
14 14
     use SerializesModels;
15 15
 
16
-    public Company $company;
17
-
18
-    /**
19
-     * Create a new event instance.
20
-     */
21
-    public function __construct(Company $company)
22
-    {
23
-        $this->company = $company;
16
+    public function __construct(
17
+        public Company $company
18
+    ) {
24 19
     }
25 20
 }

+ 3
- 5
app/Events/CompanyDefaultEvent.php 查看文件

@@ -13,13 +13,11 @@ class CompanyDefaultEvent
13 13
     use InteractsWithSockets;
14 14
     use SerializesModels;
15 15
 
16
-    public Model $model;
17
-
18 16
     /**
19 17
      * Create a new event instance.
20 18
      */
21
-    public function __construct(Model $model)
22
-    {
23
-        $this->model = $model;
19
+    public function __construct(
20
+        public Model $model
21
+    ) {
24 22
     }
25 23
 }

+ 4
- 8
app/Events/CompanyDefaultUpdated.php 查看文件

@@ -13,16 +13,12 @@ class CompanyDefaultUpdated
13 13
     use InteractsWithSockets;
14 14
     use SerializesModels;
15 15
 
16
-    public Model $record;
17
-
18
-    public array $data;
19
-
20 16
     /**
21 17
      * Create a new event instance.
22 18
      */
23
-    public function __construct(Model $record, array $data)
24
-    {
25
-        $this->record = $record;
26
-        $this->data = $data;
19
+    public function __construct(
20
+        public Model $record,
21
+        public array $data
22
+    ) {
27 23
     }
28 24
 }

+ 7
- 17
app/Events/CompanyGenerated.php 查看文件

@@ -12,25 +12,15 @@ class CompanyGenerated
12 12
     use Dispatchable;
13 13
     use SerializesModels;
14 14
 
15
-    public User $user;
16
-
17
-    public Company $company;
18
-
19
-    public string $country;
20
-
21
-    public string $language;
22
-
23
-    public string $currency;
24
-
25 15
     /**
26 16
      * Create a new event instance.
27 17
      */
28
-    public function __construct(User $user, Company $company, string $country, string $language = 'en', string $currency = 'USD')
29
-    {
30
-        $this->user = $user;
31
-        $this->company = $company;
32
-        $this->country = $country;
33
-        $this->language = $language;
34
-        $this->currency = $currency;
18
+    public function __construct(
19
+        public User $user,
20
+        public Company $company,
21
+        public string $country,
22
+        public string $language = 'en',
23
+        public string $currency = 'USD'
24
+    ) {
35 25
     }
36 26
 }

+ 5
- 14
app/Events/CurrencyRateChanged.php 查看文件

@@ -13,19 +13,10 @@ class CurrencyRateChanged
13 13
     use InteractsWithSockets;
14 14
     use SerializesModels;
15 15
 
16
-    public Currency $currency;
17
-
18
-    public float $oldRate;
19
-
20
-    public float $newRate;
21
-
22
-    /**
23
-     * Create a new event instance.
24
-     */
25
-    public function __construct(Currency $currency, float $oldRate, float $newRate)
26
-    {
27
-        $this->currency = $currency;
28
-        $this->oldRate = $oldRate;
29
-        $this->newRate = $newRate;
16
+    public function __construct(
17
+        public Currency $currency,
18
+        public float $oldRate,
19
+        public float $newRate
20
+    ) {
30 21
     }
31 22
 }

+ 3
- 5
app/Events/DefaultCurrencyChanged.php 查看文件

@@ -13,13 +13,11 @@ class DefaultCurrencyChanged
13 13
     use InteractsWithSockets;
14 14
     use SerializesModels;
15 15
 
16
-    public Currency $currency;
17
-
18 16
     /**
19 17
      * Create a new event instance.
20 18
      */
21
-    public function __construct(Currency $currency)
22
-    {
23
-        $this->currency = $currency;
19
+    public function __construct(
20
+        public Currency $currency
21
+    ) {
24 22
     }
25 23
 }

+ 5
- 11
app/Events/PlaidSuccess.php 查看文件

@@ -11,19 +11,13 @@ class PlaidSuccess
11 11
     use Dispatchable;
12 12
     use SerializesModels;
13 13
 
14
-    public string $publicToken;
15
-
16
-    public string $accessToken;
17
-
18
-    public Company $company;
19
-
20 14
     /**
21 15
      * Create a new event instance.
22 16
      */
23
-    public function __construct(string $publicToken, string $accessToken, Company $company)
24
-    {
25
-        $this->publicToken = $publicToken;
26
-        $this->accessToken = $accessToken;
27
-        $this->company = $company;
17
+    public function __construct(
18
+        public string $publicToken,
19
+        public string $accessToken,
20
+        public Company $company
21
+    ) {
28 22
     }
29 23
 }

+ 6
- 14
app/Events/StartTransactionImport.php 查看文件

@@ -12,22 +12,14 @@ class StartTransactionImport
12 12
     use Dispatchable;
13 13
     use SerializesModels;
14 14
 
15
-    public Company $company;
16
-
17
-    public ConnectedBankAccount $connectedBankAccount;
18
-
19
-    public int | string $selectedBankAccountId;
20
-
21
-    public string $startDate;
22
-
23 15
     /**
24 16
      * Create a new event instance.
25 17
      */
26
-    public function __construct(Company $company, ConnectedBankAccount $connectedBankAccount, int | string $selectedBankAccountId, string $startDate)
27
-    {
28
-        $this->company = $company;
29
-        $this->connectedBankAccount = $connectedBankAccount;
30
-        $this->selectedBankAccountId = $selectedBankAccountId;
31
-        $this->startDate = $startDate;
18
+    public function __construct(
19
+        public Company $company,
20
+        public ConnectedBankAccount $connectedBankAccount,
21
+        public int | string $selectedBankAccountId,
22
+        public string $startDate
23
+    ) {
32 24
     }
33 25
 }

+ 0
- 6
app/Facades/Accounting.php 查看文件

@@ -3,9 +3,6 @@
3 3
 namespace App\Facades;
4 4
 
5 5
 use App\Contracts\AccountHandler;
6
-use App\DTO\AccountBalanceDTO;
7
-use App\DTO\AccountBalanceReportDTO;
8
-use App\Enums\Accounting\AccountCategory;
9 6
 use App\Models\Accounting\Account;
10 7
 use App\ValueObjects\Money;
11 8
 use Illuminate\Support\Facades\Facade;
@@ -16,10 +13,7 @@ use Illuminate\Support\Facades\Facade;
16 13
  * @method static Money getNetMovement(Account $account, string $startDate, string $endDate)
17 14
  * @method static Money|null getStartingBalance(Account $account, string $startDate)
18 15
  * @method static Money|null getEndingBalance(Account $account, string $startDate, string $endDate)
19
- * @method static int calculateNetMovementByCategory(AccountCategory $category, int $debitBalance, int $creditBalance)
20 16
  * @method static array getBalances(Account $account, string $startDate, string $endDate)
21
- * @method static AccountBalanceDTO formatBalances(array $balances)
22
- * @method static AccountBalanceReportDTO buildAccountBalanceReport(string $startDate, string $endDate)
23 17
  * @method static Money getTotalBalanceForAllBankAccounts(string $startDate, string $endDate)
24 18
  * @method static array getAccountCategoryOrder()
25 19
  * @method static string getEarliestTransactionDate()

+ 2
- 1
app/Filament/Company/Pages/Reports.php 查看文件

@@ -3,6 +3,7 @@
3 3
 namespace App\Filament\Company\Pages;
4 4
 
5 5
 use App\Filament\Company\Pages\Reports\AccountBalances;
6
+use App\Filament\Company\Pages\Reports\TrialBalance;
6 7
 use App\Infolists\Components\ReportEntry;
7 8
 use Filament\Infolists\Components\Section;
8 9
 use Filament\Infolists\Infolist;
@@ -38,7 +39,7 @@ class Reports extends Page
38 39
                             ->description('The sum of all debit and credit balances for all accounts on a single day. This helps to ensure that the books are in balance.')
39 40
                             ->icon('heroicon-o-scale')
40 41
                             ->iconColor(Color::Sky)
41
-                            ->url('#'),
42
+                            ->url(TrialBalance::getUrl()),
42 43
                         ReportEntry::make('account_transactions')
43 44
                             ->hiddenLabel()
44 45
                             ->heading('Account Transactions')

+ 10
- 80
app/Filament/Company/Pages/Reports/AccountBalances.php 查看文件

@@ -2,73 +2,40 @@
2 2
 
3 3
 namespace App\Filament\Company\Pages\Reports;
4 4
 
5
-use App\DTO\AccountBalanceReportDTO;
6
-use App\Filament\Forms\Components\DateRangeSelect;
7
-use App\Models\Company;
5
+use App\DTO\ReportDTO;
8 6
 use App\Services\AccountBalancesExportService;
9
-use App\Services\AccountService;
7
+use App\Services\ReportService;
10 8
 use Barryvdh\DomPDF\Facade\Pdf;
11 9
 use Filament\Actions\Action;
12 10
 use Filament\Actions\ActionGroup;
13
-use Filament\Forms\Components\DatePicker;
14
-use Filament\Forms\Components\Split;
15
-use Filament\Forms\Form;
16
-use Filament\Forms\Set;
17
-use Filament\Pages\Page;
18 11
 use Filament\Support\Enums\IconPosition;
19 12
 use Filament\Support\Enums\IconSize;
20 13
 use Illuminate\Support\Carbon;
21 14
 use Symfony\Component\HttpFoundation\StreamedResponse;
22 15
 
23
-class AccountBalances extends Page
16
+class AccountBalances extends BaseReportPage
24 17
 {
25 18
     protected static string $view = 'filament.company.pages.reports.account-balances';
26 19
 
27 20
     protected static ?string $slug = 'reports/account-balances';
28 21
 
29
-    public string $startDate = '';
22
+    protected static bool $shouldRegisterNavigation = false;
30 23
 
31
-    public string $endDate = '';
24
+    protected ReportService $reportService;
32 25
 
33
-    public string $dateRange = '';
34
-
35
-    public string $fiscalYearStartDate = '';
36
-
37
-    public string $fiscalYearEndDate = '';
38
-
39
-    public Company $company;
40
-
41
-    public AccountBalanceReportDTO $accountBalanceReport;
42
-
43
-    protected AccountService $accountService;
26
+    public ReportDTO $accountBalanceReport;
44 27
 
45 28
     protected AccountBalancesExportService $accountBalancesExportService;
46 29
 
47
-    public function boot(AccountService $accountService, AccountBalancesExportService $accountBalancesExportService): void
30
+    public function boot(ReportService $reportService, AccountBalancesExportService $accountBalancesExportService): void
48 31
     {
49
-        $this->accountService = $accountService;
32
+        $this->reportService = $reportService;
50 33
         $this->accountBalancesExportService = $accountBalancesExportService;
51 34
     }
52 35
 
53
-    public function mount(): void
36
+    public function loadReportData(): void
54 37
     {
55
-        $this->company = auth()->user()->currentCompany;
56
-        $this->fiscalYearStartDate = $this->company->locale->fiscalYearStartDate();
57
-        $this->fiscalYearEndDate = $this->company->locale->fiscalYearEndDate();
58
-        $this->dateRange = $this->getDefaultDateRange();
59
-        $this->setDateRange(Carbon::parse($this->fiscalYearStartDate), Carbon::parse($this->fiscalYearEndDate));
60
-
61
-        $this->loadAccountBalances();
62
-    }
63
-
64
-    public function getDefaultDateRange(): string
65
-    {
66
-        return 'FY-' . now()->year;
67
-    }
68
-
69
-    public function loadAccountBalances(): void
70
-    {
71
-        $this->accountBalanceReport = $this->accountService->buildAccountBalanceReport($this->startDate, $this->endDate);
38
+        $this->accountBalanceReport = $this->reportService->buildAccountBalanceReport($this->startDate, $this->endDate);
72 39
     }
73 40
 
74 41
     protected function getHeaderActions(): array
@@ -110,41 +77,4 @@ class AccountBalances extends Page
110 77
             echo $pdf->stream();
111 78
         }, 'account-balances.pdf');
112 79
     }
113
-
114
-    public function form(Form $form): Form
115
-    {
116
-        return $form
117
-            ->schema([
118
-                Split::make([
119
-                    DateRangeSelect::make('dateRange')
120
-                        ->label('Date Range')
121
-                        ->selectablePlaceholder(false)
122
-                        ->startDateField('startDate')
123
-                        ->endDateField('endDate'),
124
-                    DatePicker::make('startDate')
125
-                        ->label('Start Date')
126
-                        ->displayFormat('Y-m-d')
127
-                        ->afterStateUpdated(static function (Set $set) {
128
-                            $set('dateRange', 'Custom');
129
-                        }),
130
-                    DatePicker::make('endDate')
131
-                        ->label('End Date')
132
-                        ->displayFormat('Y-m-d')
133
-                        ->afterStateUpdated(static function (Set $set) {
134
-                            $set('dateRange', 'Custom');
135
-                        }),
136
-                ])->live(),
137
-            ]);
138
-    }
139
-
140
-    public function setDateRange(Carbon $start, Carbon $end): void
141
-    {
142
-        $this->startDate = $start->format('Y-m-d');
143
-        $this->endDate = $end->isFuture() ? now()->format('Y-m-d') : $end->format('Y-m-d');
144
-    }
145
-
146
-    public static function shouldRegisterNavigation(): bool
147
-    {
148
-        return false;
149
-    }
150 80
 }

+ 77
- 0
app/Filament/Company/Pages/Reports/BaseReportPage.php 查看文件

@@ -0,0 +1,77 @@
1
+<?php
2
+
3
+namespace App\Filament\Company\Pages\Reports;
4
+
5
+use App\Filament\Forms\Components\DateRangeSelect;
6
+use App\Models\Company;
7
+use Filament\Forms\Components\DatePicker;
8
+use Filament\Forms\Components\Split;
9
+use Filament\Forms\Form;
10
+use Filament\Forms\Set;
11
+use Filament\Pages\Page;
12
+use Illuminate\Support\Carbon;
13
+
14
+abstract class BaseReportPage extends Page
15
+{
16
+    public string $startDate = '';
17
+
18
+    public string $endDate = '';
19
+
20
+    public string $dateRange = '';
21
+
22
+    public string $fiscalYearStartDate = '';
23
+
24
+    public string $fiscalYearEndDate = '';
25
+
26
+    public Company $company;
27
+
28
+    public function mount(): void
29
+    {
30
+        $this->company = auth()->user()->currentCompany;
31
+        $this->fiscalYearStartDate = $this->company->locale->fiscalYearStartDate();
32
+        $this->fiscalYearEndDate = $this->company->locale->fiscalYearEndDate();
33
+        $this->dateRange = $this->getDefaultDateRange();
34
+        $this->setDateRange(Carbon::parse($this->fiscalYearStartDate), Carbon::parse($this->fiscalYearEndDate));
35
+
36
+        $this->loadReportData();
37
+    }
38
+
39
+    abstract protected function loadReportData(): void;
40
+
41
+    public function getDefaultDateRange(): string
42
+    {
43
+        return 'FY-' . now()->year;
44
+    }
45
+
46
+    public function form(Form $form): Form
47
+    {
48
+        return $form
49
+            ->schema([
50
+                Split::make([
51
+                    DateRangeSelect::make('dateRange')
52
+                        ->label('Date Range')
53
+                        ->selectablePlaceholder(false)
54
+                        ->startDateField('startDate')
55
+                        ->endDateField('endDate'),
56
+                    DatePicker::make('startDate')
57
+                        ->label('Start Date')
58
+                        ->displayFormat('Y-m-d')
59
+                        ->afterStateUpdated(static function (Set $set) {
60
+                            $set('dateRange', 'Custom');
61
+                        }),
62
+                    DatePicker::make('endDate')
63
+                        ->label('End Date')
64
+                        ->displayFormat('Y-m-d')
65
+                        ->afterStateUpdated(static function (Set $set) {
66
+                            $set('dateRange', 'Custom');
67
+                        }),
68
+                ])->live(),
69
+            ]);
70
+    }
71
+
72
+    public function setDateRange(Carbon $start, Carbon $end): void
73
+    {
74
+        $this->startDate = $start->format('Y-m-d');
75
+        $this->endDate = $end->isFuture() ? now()->format('Y-m-d') : $end->format('Y-m-d');
76
+    }
77
+}

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

@@ -0,0 +1,34 @@
1
+<?php
2
+
3
+namespace App\Filament\Company\Pages\Reports;
4
+
5
+use App\DTO\ReportDTO;
6
+use App\Services\ReportService;
7
+
8
+class TrialBalance extends BaseReportPage
9
+{
10
+    protected static string $view = 'filament.company.pages.reports.trial-balance';
11
+
12
+    protected static ?string $slug = 'reports/trial-balance';
13
+
14
+    protected static bool $shouldRegisterNavigation = false;
15
+
16
+    protected ReportService $reportService;
17
+
18
+    public ReportDTO $trialBalanceReport;
19
+
20
+    public function boot(ReportService $reportService): void
21
+    {
22
+        $this->reportService = $reportService;
23
+    }
24
+
25
+    public function getDefaultDateRange(): string
26
+    {
27
+        return 'FY-' . now()->year;
28
+    }
29
+
30
+    public function loadReportData(): void
31
+    {
32
+        $this->trialBalanceReport = $this->reportService->buildTrialBalanceReport($this->startDate, $this->endDate);
33
+    }
34
+}

+ 4
- 4
app/Http/Controllers/PlaidWebhookController.php 查看文件

@@ -29,14 +29,14 @@ class PlaidWebhookController extends Controller
29 29
     private function handleDefaultUpdate(array $payload): void
30 30
     {
31 31
         $newTransactions = $payload['new_transactions'];
32
-        $itemID = $payload['item_id'];
32
+        $itemId = $payload['item_id'];
33 33
 
34
-        $company = Company::whereHas('connectedBankAccounts', static function ($query) use ($itemID) {
35
-            $query->where('item_id', $itemID);
34
+        $company = Company::whereHas('connectedBankAccounts', static function ($query) use ($itemId) {
35
+            $query->where('item_id', $itemId);
36 36
         })->first();
37 37
 
38 38
         if ($company && $newTransactions > 0) {
39
-            ProcessTransactionUpdate::dispatch($company, $itemID)
39
+            ProcessTransactionUpdate::dispatch($company, $itemId)
40 40
                 ->onQueue('transactions');
41 41
         }
42 42
     }

+ 7
- 20
app/Jobs/ProcessTransactionImport.php 查看文件

@@ -22,26 +22,13 @@ class ProcessTransactionImport implements ShouldQueue
22 22
     use Queueable;
23 23
     use SerializesModels;
24 24
 
25
-    protected Company $company;
26
-
27
-    protected Account $account;
28
-
29
-    protected BankAccount $bankAccount;
30
-
31
-    protected ConnectedBankAccount $connectedBankAccount;
32
-
33
-    protected string $startDate;
34
-
35
-    /**
36
-     * Create a new job instance.
37
-     */
38
-    public function __construct(Company $company, Account $account, BankAccount $bankAccount, ConnectedBankAccount $connectedBankAccount, string $startDate)
39
-    {
40
-        $this->company = $company;
41
-        $this->account = $account;
42
-        $this->bankAccount = $bankAccount;
43
-        $this->connectedBankAccount = $connectedBankAccount;
44
-        $this->startDate = $startDate;
25
+    public function __construct(
26
+        protected Company $company,
27
+        protected Account $account,
28
+        protected BankAccount $bankAccount,
29
+        protected ConnectedBankAccount $connectedBankAccount,
30
+        protected string $startDate
31
+    ) {
45 32
     }
46 33
 
47 34
     /**

+ 5
- 12
app/Jobs/ProcessTransactionUpdate.php 查看文件

@@ -20,17 +20,10 @@ class ProcessTransactionUpdate implements ShouldQueue
20 20
     use Queueable;
21 21
     use SerializesModels;
22 22
 
23
-    protected Company $company;
24
-
25
-    protected string $item_id;
26
-
27
-    /**
28
-     * Create a new job instance.
29
-     */
30
-    public function __construct(Company $company, string $item_id)
31
-    {
32
-        $this->company = $company;
33
-        $this->item_id = $item_id;
23
+    public function __construct(
24
+        protected Company $company,
25
+        protected string $itemId
26
+    ) {
34 27
     }
35 28
 
36 29
     /**
@@ -39,7 +32,7 @@ class ProcessTransactionUpdate implements ShouldQueue
39 32
     public function handle(PlaidService $plaidService, TransactionService $transactionService): void
40 33
     {
41 34
         $connectedBankAccounts = $this->company->connectedBankAccounts()
42
-            ->where('item_id', $this->item_id)
35
+            ->where('item_id', $this->itemId)
43 36
             ->where('import_transactions', true)
44 37
             ->get();
45 38
 

+ 5
- 7
app/Listeners/CreateConnectedAccount.php 查看文件

@@ -10,14 +10,12 @@ use Illuminate\Support\Facades\DB;
10 10
 
11 11
 class CreateConnectedAccount
12 12
 {
13
-    protected PlaidService $plaid;
14
-
15 13
     /**
16 14
      * Create the event listener.
17 15
      */
18
-    public function __construct(PlaidService $plaid)
19
-    {
20
-        $this->plaid = $plaid;
16
+    public function __construct(
17
+        protected PlaidService $plaidService
18
+    ) {
21 19
     }
22 20
 
23 21
     /**
@@ -36,9 +34,9 @@ class CreateConnectedAccount
36 34
 
37 35
         $company = $event->company;
38 36
 
39
-        $authResponse = $this->plaid->getAccounts($accessToken);
37
+        $authResponse = $this->plaidService->getAccounts($accessToken);
40 38
 
41
-        $institutionResponse = $this->plaid->getInstitution($authResponse->item->institution_id, $company->profile->country);
39
+        $institutionResponse = $this->plaidService->getInstitution($authResponse->item->institution_id, $company->profile->country);
42 40
 
43 41
         $this->processInstitution($authResponse, $institutionResponse, $company, $accessToken);
44 42
     }

+ 3
- 5
app/Listeners/HandleTransactionImport.php 查看文件

@@ -9,14 +9,12 @@ use Illuminate\Support\Facades\DB;
9 9
 
10 10
 class HandleTransactionImport
11 11
 {
12
-    protected ConnectedBankAccountService $connectedBankAccountService;
13
-
14 12
     /**
15 13
      * Create the event listener.
16 14
      */
17
-    public function __construct(ConnectedBankAccountService $connectedBankAccountService)
18
-    {
19
-        $this->connectedBankAccountService = $connectedBankAccountService;
15
+    public function __construct(
16
+        protected ConnectedBankAccountService $connectedBankAccountService
17
+    ) {
20 18
     }
21 19
 
22 20
     /**

+ 3
- 3
app/Listeners/UpdateCurrencyRates.php 查看文件

@@ -13,9 +13,9 @@ readonly class UpdateCurrencyRates
13 13
     /**
14 14
      * Create the event listener.
15 15
      */
16
-    public function __construct(private CurrencyHandler $currencyService)
17
-    {
18
-        //
16
+    public function __construct(
17
+        private CurrencyHandler $currencyService
18
+    ) {
19 19
     }
20 20
 
21 21
     /**

+ 2
- 2
app/Services/AccountBalancesExportService.php 查看文件

@@ -2,13 +2,13 @@
2 2
 
3 3
 namespace App\Services;
4 4
 
5
-use App\DTO\AccountBalanceReportDTO;
5
+use App\DTO\ReportDTO;
6 6
 use App\Models\Company;
7 7
 use Symfony\Component\HttpFoundation\StreamedResponse;
8 8
 
9 9
 class AccountBalancesExportService
10 10
 {
11
-    public function exportToCsv(Company $company, AccountBalanceReportDTO $accountBalanceReport, string $startDate, string $endDate): StreamedResponse
11
+    public function exportToCsv(Company $company, ReportDTO $accountBalanceReport, string $startDate, string $endDate): StreamedResponse
12 12
     {
13 13
         // Construct the filename
14 14
         $filename = $company->name . ' Account Balances ' . $startDate . ' to ' . $endDate . '.csv';

+ 7
- 99
app/Services/AccountService.php 查看文件

@@ -3,10 +3,6 @@
3 3
 namespace App\Services;
4 4
 
5 5
 use App\Contracts\AccountHandler;
6
-use App\DTO\AccountBalanceDTO;
7
-use App\DTO\AccountBalanceReportDTO;
8
-use App\DTO\AccountCategoryDTO;
9
-use App\DTO\AccountDTO;
10 6
 use App\Enums\Accounting\AccountCategory;
11 7
 use App\Models\Accounting\Account;
12 8
 use App\Models\Accounting\Transaction;
@@ -14,15 +10,12 @@ use App\Models\Banking\BankAccount;
14 10
 use App\Repositories\Accounting\JournalEntryRepository;
15 11
 use App\Utilities\Currency\CurrencyAccessor;
16 12
 use App\ValueObjects\Money;
17
-use Illuminate\Database\Eloquent\Collection;
18 13
 
19 14
 class AccountService implements AccountHandler
20 15
 {
21
-    protected JournalEntryRepository $journalEntryRepository;
22
-
23
-    public function __construct(JournalEntryRepository $journalEntryRepository)
24
-    {
25
-        $this->journalEntryRepository = $journalEntryRepository;
16
+    public function __construct(
17
+        protected JournalEntryRepository $journalEntryRepository
18
+    ) {
26 19
     }
27 20
 
28 21
     public function getDebitBalance(Account $account, string $startDate, string $endDate): Money
@@ -63,18 +56,19 @@ class AccountService implements AccountHandler
63 56
 
64 57
     public function getEndingBalance(Account $account, string $startDate, string $endDate): ?Money
65 58
     {
59
+        $netMovement = $this->getNetMovement($account, $startDate, $endDate)->getAmount();
60
+
66 61
         if (in_array($account->category, [AccountCategory::Expense, AccountCategory::Revenue], true)) {
67
-            return null;
62
+            return new Money($netMovement, $account->currency_code);
68 63
         }
69 64
 
70 65
         $startingBalance = $this->getStartingBalance($account, $startDate)?->getAmount();
71
-        $netMovement = $this->getNetMovement($account, $startDate, $endDate)->getAmount();
72 66
         $endingBalance = $startingBalance + $netMovement;
73 67
 
74 68
         return new Money($endingBalance, $account->currency_code);
75 69
     }
76 70
 
77
-    public function calculateNetMovementByCategory(AccountCategory $category, int $debitBalance, int $creditBalance): int
71
+    private function calculateNetMovementByCategory(AccountCategory $category, int $debitBalance, int $creditBalance): int
78 72
     {
79 73
         return match ($category) {
80 74
             AccountCategory::Asset, AccountCategory::Expense => $debitBalance - $creditBalance,
@@ -102,91 +96,6 @@ class AccountService implements AccountHandler
102 96
         return $balances;
103 97
     }
104 98
 
105
-    public function formatBalances(array $balances): AccountBalanceDTO
106
-    {
107
-        $defaultCurrency = CurrencyAccessor::getDefaultCurrency();
108
-
109
-        foreach ($balances as $key => $balance) {
110
-            $balances[$key] = money($balance, $defaultCurrency)->format();
111
-        }
112
-
113
-        return new AccountBalanceDTO(
114
-            startingBalance: $balances['starting_balance'] ?? null,
115
-            debitBalance: $balances['debit_balance'],
116
-            creditBalance: $balances['credit_balance'],
117
-            netMovement: $balances['net_movement'] ?? null,
118
-            endingBalance: $balances['ending_balance'] ?? null,
119
-        );
120
-    }
121
-
122
-    public function buildAccountBalanceReport(string $startDate, string $endDate): AccountBalanceReportDTO
123
-    {
124
-        $allCategories = $this->getAccountCategoryOrder();
125
-
126
-        $categoryGroupedAccounts = Account::whereHas('journalEntries')
127
-            ->select('id', 'name', 'currency_code', 'category', 'code')
128
-            ->get()
129
-            ->groupBy(fn (Account $account) => $account->category->getPluralLabel())
130
-            ->sortBy(static fn (Collection $groupedAccounts, string $key) => array_search($key, $allCategories, true));
131
-
132
-        $accountCategories = [];
133
-        $reportTotalBalances = [
134
-            'debit_balance' => 0,
135
-            'credit_balance' => 0,
136
-        ];
137
-
138
-        foreach ($allCategories as $categoryName) {
139
-            $accountsInCategory = $categoryGroupedAccounts[$categoryName] ?? collect();
140
-            $categorySummaryBalances = [
141
-                'debit_balance' => 0,
142
-                'credit_balance' => 0,
143
-                'net_movement' => 0,
144
-            ];
145
-
146
-            if (! in_array($categoryName, [AccountCategory::Expense->getPluralLabel(), AccountCategory::Revenue->getPluralLabel()], true)) {
147
-                $categorySummaryBalances['starting_balance'] = 0;
148
-                $categorySummaryBalances['ending_balance'] = 0;
149
-            }
150
-
151
-            $categoryAccounts = [];
152
-
153
-            foreach ($accountsInCategory as $account) {
154
-                /** @var Account $account */
155
-                $accountBalances = $this->getBalances($account, $startDate, $endDate);
156
-
157
-                if (array_sum($accountBalances) === 0) {
158
-                    continue;
159
-                }
160
-
161
-                foreach ($accountBalances as $accountBalanceType => $accountBalance) {
162
-                    $categorySummaryBalances[$accountBalanceType] += $accountBalance;
163
-                }
164
-
165
-                $formattedAccountBalances = $this->formatBalances($accountBalances);
166
-
167
-                $categoryAccounts[] = new AccountDTO(
168
-                    $account->name,
169
-                    $account->code,
170
-                    $formattedAccountBalances,
171
-                );
172
-            }
173
-
174
-            $reportTotalBalances['debit_balance'] += $categorySummaryBalances['debit_balance'];
175
-            $reportTotalBalances['credit_balance'] += $categorySummaryBalances['credit_balance'];
176
-
177
-            $formattedCategorySummaryBalances = $this->formatBalances($categorySummaryBalances);
178
-
179
-            $accountCategories[$categoryName] = new AccountCategoryDTO(
180
-                $categoryAccounts,
181
-                $formattedCategorySummaryBalances,
182
-            );
183
-        }
184
-
185
-        $formattedReportTotalBalances = $this->formatBalances($reportTotalBalances);
186
-
187
-        return new AccountBalanceReportDTO($accountCategories, $formattedReportTotalBalances);
188
-    }
189
-
190 99
     public function getTotalBalanceForAllBankAccounts(string $startDate, string $endDate): Money
191 100
     {
192 101
         $bankAccounts = BankAccount::with('account')
@@ -194,7 +103,6 @@ class AccountService implements AccountHandler
194 103
 
195 104
         $totalBalance = 0;
196 105
 
197
-        // Get ending balance for each bank account
198 106
         foreach ($bankAccounts as $bankAccount) {
199 107
             $account = $bankAccount->account;
200 108
 

+ 4
- 8
app/Services/ConnectedBankAccountService.php 查看文件

@@ -11,14 +11,10 @@ use App\Repositories\Banking\ConnectedBankAccountRepository;
11 11
 
12 12
 class ConnectedBankAccountService
13 13
 {
14
-    protected AccountSubtypeRepository $accountSubtypeRepository;
15
-
16
-    protected ConnectedBankAccountRepository $connectedBankAccountRepository;
17
-
18
-    public function __construct(AccountSubtypeRepository $accountSubtypeRepository, ConnectedBankAccountRepository $connectedBankAccountRepository)
19
-    {
20
-        $this->accountSubtypeRepository = $accountSubtypeRepository;
21
-        $this->connectedBankAccountRepository = $connectedBankAccountRepository;
14
+    public function __construct(
15
+        protected AccountSubtypeRepository $accountSubtypeRepository,
16
+        protected ConnectedBankAccountRepository $connectedBankAccountRepository
17
+    ) {
22 18
     }
23 19
 
24 20
     public function getOrProcessBankAccountForConnectedBankAccount(Company $company, ConnectedBankAccount $connectedBankAccount, int | string $selectedBankAccountId): BankAccount

+ 8
- 14
app/Services/CurrencyService.php 查看文件

@@ -10,17 +10,11 @@ use Illuminate\Support\Facades\Log;
10 10
 
11 11
 class CurrencyService implements CurrencyHandler
12 12
 {
13
-    protected ?string $api_key;
14
-
15
-    protected ?string $base_url;
16
-
17
-    protected Client $client;
18
-
19
-    public function __construct(?string $api_key, ?string $base_url, Client $client)
20
-    {
21
-        $this->api_key = $api_key;
22
-        $this->base_url = $base_url;
23
-        $this->client = $client;
13
+    public function __construct(
14
+        protected ?string $apiKey,
15
+        protected ?string $baseUrl,
16
+        protected Client $client
17
+    ) {
24 18
     }
25 19
 
26 20
     /**
@@ -28,7 +22,7 @@ class CurrencyService implements CurrencyHandler
28 22
      */
29 23
     public function isEnabled(): bool
30 24
     {
31
-        return filled($this->api_key) && filled($this->base_url);
25
+        return filled($this->apiKey) && filled($this->baseUrl);
32 26
     }
33 27
 
34 28
     public function getSupportedCurrencies(): ?array
@@ -38,7 +32,7 @@ class CurrencyService implements CurrencyHandler
38 32
         }
39 33
 
40 34
         return Cache::remember('supported_currency_codes', now()->addMonth(), function () {
41
-            $response = $this->client->get("{$this->base_url}/{$this->api_key}/codes");
35
+            $response = $this->client->get("{$this->baseUrl}/{$this->apiKey}/codes");
42 36
 
43 37
             if ($response->getStatusCode() === 200) {
44 38
                 $responseData = json_decode($response->getBody()->getContents(), true);
@@ -106,7 +100,7 @@ class CurrencyService implements CurrencyHandler
106 100
     public function updateCurrencyRatesCache(string $baseCurrency): ?array
107 101
     {
108 102
         try {
109
-            $response = $this->client->get("{$this->base_url}/{$this->api_key}/latest/{$baseCurrency}");
103
+            $response = $this->client->get("{$this->baseUrl}/{$this->apiKey}/latest/{$baseCurrency}");
110 104
 
111 105
             if ($response->getStatusCode() === 200) {
112 106
                 $responseData = json_decode($response->getBody()->getContents(), true);

+ 47
- 37
app/Services/PlaidService.php 查看文件

@@ -15,15 +15,15 @@ class PlaidService
15 15
 {
16 16
     public const API_VERSION = '2020-09-14';
17 17
 
18
-    protected ?string $client_id;
18
+    protected ?string $clientId;
19 19
 
20
-    protected ?string $client_secret;
20
+    protected ?string $clientSecret;
21 21
 
22 22
     protected ?string $environment;
23 23
 
24
-    protected ?string $webhook_url;
24
+    protected ?string $webhookUrl;
25 25
 
26
-    protected ?string $base_url;
26
+    protected ?string $baseUrl;
27 27
 
28 28
     protected HttpClient $client;
29 29
 
@@ -50,18 +50,18 @@ class PlaidService
50 50
     {
51 51
         $this->client = $client;
52 52
         $this->config = $config;
53
-        $this->client_id = $this->config->get('plaid.client_id');
54
-        $this->client_secret = $this->config->get('plaid.client_secret');
53
+        $this->clientId = $this->config->get('plaid.client_id');
54
+        $this->clientSecret = $this->config->get('plaid.client_secret');
55 55
         $this->environment = $this->config->get('plaid.environment', 'sandbox');
56
-        $this->webhook_url = $this->config->get('plaid.webhook_url');
56
+        $this->webhookUrl = $this->config->get('plaid.webhook_url');
57 57
 
58 58
         $this->setBaseUrl($this->environment);
59 59
     }
60 60
 
61
-    public function setClientCredentials(?string $client_id, ?string $client_secret): self
61
+    public function setClientCredentials(?string $clientId, ?string $clientSecret): self
62 62
     {
63
-        $this->client_id = $client_id ?? $this->client_id;
64
-        $this->client_secret = $client_secret ?? $this->client_secret;
63
+        $this->clientId = $clientId ?? $this->clientId;
64
+        $this->clientSecret = $clientSecret ?? $this->clientSecret;
65 65
 
66 66
         return $this;
67 67
     }
@@ -77,7 +77,7 @@ class PlaidService
77 77
 
78 78
     public function setBaseUrl(?string $environment): void
79 79
     {
80
-        $this->base_url = match ($environment) {
80
+        $this->baseUrl = match ($environment) {
81 81
             'development' => 'https://development.plaid.com',
82 82
             'production' => 'https://production.plaid.com',
83 83
             default => 'https://sandbox.plaid.com', // Default to sandbox, including if environment is null
@@ -86,7 +86,7 @@ class PlaidService
86 86
 
87 87
     public function getBaseUrl(): string
88 88
     {
89
-        return $this->base_url;
89
+        return $this->baseUrl;
90 90
     }
91 91
 
92 92
     public function getEnvironment(): string
@@ -99,12 +99,12 @@ class PlaidService
99 99
         $request = $this->client->withHeaders([
100 100
             'Plaid-Version' => self::API_VERSION,
101 101
             'Content-Type' => 'application/json',
102
-        ])->baseUrl($this->base_url);
102
+        ])->baseUrl($this->baseUrl);
103 103
 
104 104
         if ($method === 'post') {
105 105
             $request = $request->withHeaders([
106
-                'PLAID-CLIENT-ID' => $this->client_id,
107
-                'PLAID-SECRET' => $this->client_secret,
106
+                'PLAID-CLIENT-ID' => $this->clientId,
107
+                'PLAID-SECRET' => $this->clientSecret,
108 108
             ]);
109 109
         }
110 110
 
@@ -181,12 +181,12 @@ class PlaidService
181 181
         );
182 182
     }
183 183
 
184
-    public function createLinkToken(string $client_name, string $language, array $country_codes, array $user, array $products): object
184
+    public function createLinkToken(string $clientName, string $language, array $countryCodes, array $user, array $products): object
185 185
     {
186 186
         $data = [
187
-            'client_name' => $client_name,
187
+            'client_name' => $clientName,
188 188
             'language' => $language,
189
-            'country_codes' => $country_codes,
189
+            'country_codes' => $countryCodes,
190 190
             'user' => (object) $user,
191 191
         ];
192 192
 
@@ -194,16 +194,18 @@ class PlaidService
194 194
             $data['products'] = $products;
195 195
         }
196 196
 
197
-        if (! empty($this->webhook_url)) {
198
-            $data['webhook'] = $this->webhook_url;
197
+        if (! empty($this->webhookUrl)) {
198
+            $data['webhook'] = $this->webhookUrl;
199 199
         }
200 200
 
201 201
         return $this->sendRequest('link/token/create', $data);
202 202
     }
203 203
 
204
-    public function exchangePublicToken(string $public_token): object
204
+    public function exchangePublicToken(string $publicToken): object
205 205
     {
206
-        $data = compact('public_token');
206
+        $data = [
207
+            'public_token' => $publicToken,
208
+        ];
207 209
 
208 210
         return $this->sendRequest('item/public_token/exchange', $data);
209 211
     }
@@ -218,7 +220,7 @@ class PlaidService
218 220
         return $this->sendRequest('accounts/get', $data);
219 221
     }
220 222
 
221
-    public function getInstitution(string $institution_id, string $country): object
223
+    public function getInstitution(string $institutionId, string $country): object
222 224
     {
223 225
         $options = [
224 226
             'include_optional_metadata' => true,
@@ -226,49 +228,57 @@ class PlaidService
226 228
 
227 229
         $plaidCountry = $this->getCountry($country);
228 230
 
229
-        return $this->getInstitutionById($institution_id, [$plaidCountry], $options);
231
+        return $this->getInstitutionById($institutionId, [$plaidCountry], $options);
230 232
     }
231 233
 
232
-    public function getInstitutionById(string $institution_id, array $country_codes, array $options = []): object
234
+    public function getInstitutionById(string $institutionId, array $countryCodes, array $options = []): object
233 235
     {
234 236
         $data = [
235
-            'institution_id' => $institution_id,
236
-            'country_codes' => $country_codes,
237
+            'institution_id' => $institutionId,
238
+            'country_codes' => $countryCodes,
237 239
             'options' => (object) $options,
238 240
         ];
239 241
 
240 242
         return $this->sendRequest('institutions/get_by_id', $data);
241 243
     }
242 244
 
243
-    public function getTransactions(string $access_token, string $start_date, string $end_date, array $options = []): object
245
+    public function getTransactions(string $accessToken, string $startDate, string $endDate, array $options = []): object
244 246
     {
245 247
         $data = [
246
-            'access_token' => $access_token,
247
-            'start_date' => $start_date,
248
-            'end_date' => $end_date,
248
+            'access_token' => $accessToken,
249
+            'start_date' => $startDate,
250
+            'end_date' => $endDate,
249 251
             'options' => (object) $options,
250 252
         ];
251 253
 
252 254
         return $this->sendRequest('transactions/get', $data);
253 255
     }
254 256
 
255
-    public function fireSandboxWebhook(string $access_token, string $webhook_code, string $webhook_type): object
257
+    public function fireSandboxWebhook(string $accessToken, string $webhookCode, string $webhookType): object
256 258
     {
257
-        $data = compact('access_token', 'webhook_code', 'webhook_type');
259
+        $data = [
260
+            'access_token' => $accessToken,
261
+            'webhook_code' => $webhookCode,
262
+            'webhook_type' => $webhookType,
263
+        ];
258 264
 
259 265
         return $this->sendRequest('sandbox/item/fire_webhook', $data);
260 266
     }
261 267
 
262
-    public function refreshTransactions(string $access_token): object
268
+    public function refreshTransactions(string $accessToken): object
263 269
     {
264
-        $data = compact('access_token');
270
+        $data = [
271
+            'access_token' => $accessToken,
272
+        ];
265 273
 
266 274
         return $this->sendRequest('transactions/refresh', $data);
267 275
     }
268 276
 
269
-    public function removeItem(string $access_token): object
277
+    public function removeItem(string $accessToken): object
270 278
     {
271
-        $data = compact('access_token');
279
+        $data = [
280
+            'access_token' => $accessToken,
281
+        ];
272 282
 
273 283
         return $this->sendRequest('item/remove', $data);
274 284
     }

+ 170
- 0
app/Services/ReportService.php 查看文件

@@ -0,0 +1,170 @@
1
+<?php
2
+
3
+namespace App\Services;
4
+
5
+use App\DTO\AccountBalanceDTO;
6
+use App\DTO\AccountCategoryDTO;
7
+use App\DTO\AccountDTO;
8
+use App\DTO\ReportDTO;
9
+use App\Enums\Accounting\AccountCategory;
10
+use App\Models\Accounting\Account;
11
+use App\Utilities\Currency\CurrencyAccessor;
12
+use Illuminate\Database\Eloquent\Collection;
13
+
14
+class ReportService
15
+{
16
+    public function __construct(
17
+        protected AccountService $accountService,
18
+    ) {
19
+    }
20
+
21
+    public function formatBalances(array $balances): AccountBalanceDTO
22
+    {
23
+        $defaultCurrency = CurrencyAccessor::getDefaultCurrency();
24
+
25
+        foreach ($balances as $key => $balance) {
26
+            $balances[$key] = money($balance, $defaultCurrency)->format();
27
+        }
28
+
29
+        return new AccountBalanceDTO(
30
+            startingBalance: $balances['starting_balance'] ?? null,
31
+            debitBalance: $balances['debit_balance'],
32
+            creditBalance: $balances['credit_balance'],
33
+            netMovement: $balances['net_movement'] ?? null,
34
+            endingBalance: $balances['ending_balance'] ?? null,
35
+        );
36
+    }
37
+
38
+    private function filterBalances(array $balances, array $fields): array
39
+    {
40
+        return array_filter($balances, static fn ($key) => in_array($key, $fields, true), ARRAY_FILTER_USE_KEY);
41
+    }
42
+
43
+    private function getCategoryGroupedAccounts(array $allCategories): Collection
44
+    {
45
+        return Account::whereHas('journalEntries')
46
+            ->select('id', 'name', 'currency_code', 'category', 'code')
47
+            ->get()
48
+            ->groupBy(fn (Account $account) => $account->category->getPluralLabel())
49
+            ->sortBy(static fn (Collection $groupedAccounts, string $key) => array_search($key, $allCategories, true));
50
+    }
51
+
52
+    private function buildReport(array $allCategories, Collection $categoryGroupedAccounts, callable $balanceCalculator, array $fields, ?callable $initializeCategoryBalances = null): ReportDTO
53
+    {
54
+        $accountCategories = [];
55
+        $reportTotalBalances = array_fill_keys($fields, 0);
56
+
57
+        foreach ($allCategories as $categoryName) {
58
+            $accountsInCategory = $categoryGroupedAccounts[$categoryName] ?? collect();
59
+            $categorySummaryBalances = array_fill_keys($fields, 0);
60
+
61
+            if ($initializeCategoryBalances) {
62
+                $initializeCategoryBalances($categoryName, $categorySummaryBalances);
63
+            }
64
+
65
+            $categoryAccounts = [];
66
+
67
+            foreach ($accountsInCategory as $account) {
68
+                /** @var Account $account */
69
+                $accountBalances = $balanceCalculator($account);
70
+
71
+                if (array_sum($accountBalances) === 0) {
72
+                    continue;
73
+                }
74
+
75
+                foreach ($accountBalances as $accountBalanceType => $accountBalance) {
76
+                    if (array_key_exists($accountBalanceType, $categorySummaryBalances)) {
77
+                        $categorySummaryBalances[$accountBalanceType] += $accountBalance;
78
+                    }
79
+                }
80
+
81
+                $filteredAccountBalances = $this->filterBalances($accountBalances, $fields);
82
+                $formattedAccountBalances = $this->formatBalances($filteredAccountBalances);
83
+
84
+                $categoryAccounts[] = new AccountDTO(
85
+                    $account->name,
86
+                    $account->code,
87
+                    $formattedAccountBalances,
88
+                );
89
+            }
90
+
91
+            foreach ($fields as $field) {
92
+                if (array_key_exists($field, $categorySummaryBalances)) {
93
+                    $reportTotalBalances[$field] += $categorySummaryBalances[$field];
94
+                }
95
+            }
96
+
97
+            $filteredCategorySummaryBalances = $this->filterBalances($categorySummaryBalances, $fields);
98
+            $formattedCategorySummaryBalances = $this->formatBalances($filteredCategorySummaryBalances);
99
+
100
+            $accountCategories[$categoryName] = new AccountCategoryDTO(
101
+                $categoryAccounts,
102
+                $formattedCategorySummaryBalances,
103
+            );
104
+        }
105
+
106
+        $formattedReportTotalBalances = $this->formatBalances($reportTotalBalances);
107
+
108
+        return new ReportDTO($accountCategories, $formattedReportTotalBalances);
109
+    }
110
+
111
+    public function buildAccountBalanceReport(string $startDate, string $endDate): ReportDTO
112
+    {
113
+        $allCategories = $this->accountService->getAccountCategoryOrder();
114
+
115
+        $categoryGroupedAccounts = $this->getCategoryGroupedAccounts($allCategories);
116
+
117
+        $fields = ['starting_balance', 'debit_balance', 'credit_balance', 'net_movement', 'ending_balance'];
118
+
119
+        return $this->buildReport(
120
+            $allCategories,
121
+            $categoryGroupedAccounts,
122
+            fn (Account $account) => $this->accountService->getBalances($account, $startDate, $endDate),
123
+            $fields,
124
+            fn (string $categoryName, array &$categorySummaryBalances) => $this->adjustAccountBalanceCategoryFields($categoryName, $categorySummaryBalances),
125
+        );
126
+    }
127
+
128
+    private function adjustAccountBalanceCategoryFields(string $categoryName, array &$categorySummaryBalances): void
129
+    {
130
+        if (in_array($categoryName, [AccountCategory::Expense->getPluralLabel(), AccountCategory::Revenue->getPluralLabel()], true)) {
131
+            unset($categorySummaryBalances['starting_balance'], $categorySummaryBalances['ending_balance']);
132
+        }
133
+    }
134
+
135
+    public function buildTrialBalanceReport(string $startDate, string $endDate): ReportDTO
136
+    {
137
+        $allCategories = $this->accountService->getAccountCategoryOrder();
138
+
139
+        $categoryGroupedAccounts = $this->getCategoryGroupedAccounts($allCategories);
140
+
141
+        $fields = ['debit_balance', 'credit_balance'];
142
+
143
+        return $this->buildReport($allCategories, $categoryGroupedAccounts, function (Account $account) use ($startDate, $endDate) {
144
+            $endingBalance = $this->accountService->getEndingBalance($account, $startDate, $endDate)?->getAmount() ?? 0;
145
+
146
+            if ($endingBalance === 0) {
147
+                return [];
148
+            }
149
+
150
+            return $this->calculateTrialBalance($account->category, $endingBalance);
151
+        }, $fields);
152
+    }
153
+
154
+    private function calculateTrialBalance(AccountCategory $category, int $endingBalance): array
155
+    {
156
+        if (in_array($category, [AccountCategory::Asset, AccountCategory::Expense], true)) {
157
+            if ($endingBalance >= 0) {
158
+                return ['debit_balance' => $endingBalance, 'credit_balance' => 0];
159
+            }
160
+
161
+            return ['debit_balance' => 0, 'credit_balance' => abs($endingBalance)];
162
+        }
163
+
164
+        if ($endingBalance >= 0) {
165
+            return ['debit_balance' => 0, 'credit_balance' => $endingBalance];
166
+        }
167
+
168
+        return ['debit_balance' => abs($endingBalance), 'credit_balance' => 0];
169
+    }
170
+}

+ 4
- 8
app/ValueObjects/Money.php 查看文件

@@ -7,16 +7,12 @@ use App\Utilities\Currency\CurrencyConverter;
7 7
 
8 8
 class Money
9 9
 {
10
-    private int $amount;
11
-
12
-    private string $currencyCode;
13
-
14 10
     private ?int $convertedAmount = null;
15 11
 
16
-    public function __construct(int $amount, string $currencyCode)
17
-    {
18
-        $this->amount = $amount;
19
-        $this->currencyCode = $currencyCode;
12
+    public function __construct(
13
+        private readonly int $amount,
14
+        private readonly string $currencyCode
15
+    ) {
20 16
     }
21 17
 
22 18
     public function getAmount(): int

+ 4
- 8
app/View/Models/InvoiceViewModel.php 查看文件

@@ -11,14 +11,10 @@ class InvoiceViewModel
11 11
 {
12 12
     use HasFont;
13 13
 
14
-    public DocumentDefault $invoice;
15
-
16
-    public ?array $data = [];
17
-
18
-    public function __construct(DocumentDefault $invoice, ?array $data = null)
19
-    {
20
-        $this->invoice = $invoice;
21
-        $this->data = $data;
14
+    public function __construct(
15
+        public DocumentDefault $invoice,
16
+        public ?array $data = null
17
+    ) {
22 18
     }
23 19
 
24 20
     public function logo(): ?string

+ 53
- 54
composer.lock 查看文件

@@ -497,16 +497,16 @@
497 497
         },
498 498
         {
499 499
             "name": "aws/aws-sdk-php",
500
-            "version": "3.308.1",
500
+            "version": "3.308.3",
501 501
             "source": {
502 502
                 "type": "git",
503 503
                 "url": "https://github.com/aws/aws-sdk-php.git",
504
-                "reference": "bf5f1221d4c5c67d3213150fb91dfb5ce627227b"
504
+                "reference": "7fa0625056fa1fcf6732f89ba37b3f630d78de59"
505 505
             },
506 506
             "dist": {
507 507
                 "type": "zip",
508
-                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/bf5f1221d4c5c67d3213150fb91dfb5ce627227b",
509
-                "reference": "bf5f1221d4c5c67d3213150fb91dfb5ce627227b",
508
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/7fa0625056fa1fcf6732f89ba37b3f630d78de59",
509
+                "reference": "7fa0625056fa1fcf6732f89ba37b3f630d78de59",
510 510
                 "shasum": ""
511 511
             },
512 512
             "require": {
@@ -586,9 +586,9 @@
586 586
             "support": {
587 587
                 "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
588 588
                 "issues": "https://github.com/aws/aws-sdk-php/issues",
589
-                "source": "https://github.com/aws/aws-sdk-php/tree/3.308.1"
589
+                "source": "https://github.com/aws/aws-sdk-php/tree/3.308.3"
590 590
             },
591
-            "time": "2024-05-22T18:05:56+00:00"
591
+            "time": "2024-05-24T18:29:40+00:00"
592 592
         },
593 593
         {
594 594
             "name": "aws/aws-sdk-php-laravel",
@@ -1985,16 +1985,16 @@
1985 1985
         },
1986 1986
         {
1987 1987
             "name": "filament/actions",
1988
-            "version": "v3.2.81",
1988
+            "version": "v3.2.82",
1989 1989
             "source": {
1990 1990
                 "type": "git",
1991 1991
                 "url": "https://github.com/filamentphp/actions.git",
1992
-                "reference": "05e19bf192dfc7eb13989543c4fae4c41ee08124"
1992
+                "reference": "889cb3538090cfe0eef1ca51ce7808646133b7ed"
1993 1993
             },
1994 1994
             "dist": {
1995 1995
                 "type": "zip",
1996
-                "url": "https://api.github.com/repos/filamentphp/actions/zipball/05e19bf192dfc7eb13989543c4fae4c41ee08124",
1997
-                "reference": "05e19bf192dfc7eb13989543c4fae4c41ee08124",
1996
+                "url": "https://api.github.com/repos/filamentphp/actions/zipball/889cb3538090cfe0eef1ca51ce7808646133b7ed",
1997
+                "reference": "889cb3538090cfe0eef1ca51ce7808646133b7ed",
1998 1998
                 "shasum": ""
1999 1999
             },
2000 2000
             "require": {
@@ -2034,20 +2034,20 @@
2034 2034
                 "issues": "https://github.com/filamentphp/filament/issues",
2035 2035
                 "source": "https://github.com/filamentphp/filament"
2036 2036
             },
2037
-            "time": "2024-05-20T16:26:41+00:00"
2037
+            "time": "2024-05-23T12:08:12+00:00"
2038 2038
         },
2039 2039
         {
2040 2040
             "name": "filament/filament",
2041
-            "version": "v3.2.81",
2041
+            "version": "v3.2.82",
2042 2042
             "source": {
2043 2043
                 "type": "git",
2044 2044
                 "url": "https://github.com/filamentphp/panels.git",
2045
-                "reference": "3fb5cbeb32b02ef196a750441c7ac452c273efab"
2045
+                "reference": "b73e49d6ef02dba8f118d2934435748ed4feffa2"
2046 2046
             },
2047 2047
             "dist": {
2048 2048
                 "type": "zip",
2049
-                "url": "https://api.github.com/repos/filamentphp/panels/zipball/3fb5cbeb32b02ef196a750441c7ac452c273efab",
2050
-                "reference": "3fb5cbeb32b02ef196a750441c7ac452c273efab",
2049
+                "url": "https://api.github.com/repos/filamentphp/panels/zipball/b73e49d6ef02dba8f118d2934435748ed4feffa2",
2050
+                "reference": "b73e49d6ef02dba8f118d2934435748ed4feffa2",
2051 2051
                 "shasum": ""
2052 2052
             },
2053 2053
             "require": {
@@ -2099,20 +2099,20 @@
2099 2099
                 "issues": "https://github.com/filamentphp/filament/issues",
2100 2100
                 "source": "https://github.com/filamentphp/filament"
2101 2101
             },
2102
-            "time": "2024-05-20T16:26:45+00:00"
2102
+            "time": "2024-05-23T12:08:29+00:00"
2103 2103
         },
2104 2104
         {
2105 2105
             "name": "filament/forms",
2106
-            "version": "v3.2.81",
2106
+            "version": "v3.2.82",
2107 2107
             "source": {
2108 2108
                 "type": "git",
2109 2109
                 "url": "https://github.com/filamentphp/forms.git",
2110
-                "reference": "9ab84e46226acdbbf48e1f73fa1d141d566eab73"
2110
+                "reference": "c97fb3c5a6e5dd0d4d9cdbaa8b43c99c0cc35a01"
2111 2111
             },
2112 2112
             "dist": {
2113 2113
                 "type": "zip",
2114
-                "url": "https://api.github.com/repos/filamentphp/forms/zipball/9ab84e46226acdbbf48e1f73fa1d141d566eab73",
2115
-                "reference": "9ab84e46226acdbbf48e1f73fa1d141d566eab73",
2114
+                "url": "https://api.github.com/repos/filamentphp/forms/zipball/c97fb3c5a6e5dd0d4d9cdbaa8b43c99c0cc35a01",
2115
+                "reference": "c97fb3c5a6e5dd0d4d9cdbaa8b43c99c0cc35a01",
2116 2116
                 "shasum": ""
2117 2117
             },
2118 2118
             "require": {
@@ -2155,20 +2155,20 @@
2155 2155
                 "issues": "https://github.com/filamentphp/filament/issues",
2156 2156
                 "source": "https://github.com/filamentphp/filament"
2157 2157
             },
2158
-            "time": "2024-05-20T16:26:41+00:00"
2158
+            "time": "2024-05-23T12:08:13+00:00"
2159 2159
         },
2160 2160
         {
2161 2161
             "name": "filament/infolists",
2162
-            "version": "v3.2.81",
2162
+            "version": "v3.2.82",
2163 2163
             "source": {
2164 2164
                 "type": "git",
2165 2165
                 "url": "https://github.com/filamentphp/infolists.git",
2166
-                "reference": "214c92c446277bb6599758a29ad885e79b11e6b0"
2166
+                "reference": "d473d1b11ad32c917cf6e3610411fd790b8c1e27"
2167 2167
             },
2168 2168
             "dist": {
2169 2169
                 "type": "zip",
2170
-                "url": "https://api.github.com/repos/filamentphp/infolists/zipball/214c92c446277bb6599758a29ad885e79b11e6b0",
2171
-                "reference": "214c92c446277bb6599758a29ad885e79b11e6b0",
2170
+                "url": "https://api.github.com/repos/filamentphp/infolists/zipball/d473d1b11ad32c917cf6e3610411fd790b8c1e27",
2171
+                "reference": "d473d1b11ad32c917cf6e3610411fd790b8c1e27",
2172 2172
                 "shasum": ""
2173 2173
             },
2174 2174
             "require": {
@@ -2206,11 +2206,11 @@
2206 2206
                 "issues": "https://github.com/filamentphp/filament/issues",
2207 2207
                 "source": "https://github.com/filamentphp/filament"
2208 2208
             },
2209
-            "time": "2024-05-16T11:11:37+00:00"
2209
+            "time": "2024-05-23T12:08:25+00:00"
2210 2210
         },
2211 2211
         {
2212 2212
             "name": "filament/notifications",
2213
-            "version": "v3.2.81",
2213
+            "version": "v3.2.82",
2214 2214
             "source": {
2215 2215
                 "type": "git",
2216 2216
                 "url": "https://github.com/filamentphp/notifications.git",
@@ -2262,16 +2262,16 @@
2262 2262
         },
2263 2263
         {
2264 2264
             "name": "filament/support",
2265
-            "version": "v3.2.81",
2265
+            "version": "v3.2.82",
2266 2266
             "source": {
2267 2267
                 "type": "git",
2268 2268
                 "url": "https://github.com/filamentphp/support.git",
2269
-                "reference": "1435caac4c2a454ab0eb11fc6590628eda36aa0b"
2269
+                "reference": "f32b841309d01c4538d8f128d67ec213969747e9"
2270 2270
             },
2271 2271
             "dist": {
2272 2272
                 "type": "zip",
2273
-                "url": "https://api.github.com/repos/filamentphp/support/zipball/1435caac4c2a454ab0eb11fc6590628eda36aa0b",
2274
-                "reference": "1435caac4c2a454ab0eb11fc6590628eda36aa0b",
2273
+                "url": "https://api.github.com/repos/filamentphp/support/zipball/f32b841309d01c4538d8f128d67ec213969747e9",
2274
+                "reference": "f32b841309d01c4538d8f128d67ec213969747e9",
2275 2275
                 "shasum": ""
2276 2276
             },
2277 2277
             "require": {
@@ -2316,20 +2316,20 @@
2316 2316
                 "issues": "https://github.com/filamentphp/filament/issues",
2317 2317
                 "source": "https://github.com/filamentphp/filament"
2318 2318
             },
2319
-            "time": "2024-05-20T16:26:41+00:00"
2319
+            "time": "2024-05-23T12:08:37+00:00"
2320 2320
         },
2321 2321
         {
2322 2322
             "name": "filament/tables",
2323
-            "version": "v3.2.81",
2323
+            "version": "v3.2.82",
2324 2324
             "source": {
2325 2325
                 "type": "git",
2326 2326
                 "url": "https://github.com/filamentphp/tables.git",
2327
-                "reference": "9f0141c7076c096b90cf1e22b45d7ddc3db9c8d5"
2327
+                "reference": "6c183a98ee19f491f229dbca732f0b00ad9e31be"
2328 2328
             },
2329 2329
             "dist": {
2330 2330
                 "type": "zip",
2331
-                "url": "https://api.github.com/repos/filamentphp/tables/zipball/9f0141c7076c096b90cf1e22b45d7ddc3db9c8d5",
2332
-                "reference": "9f0141c7076c096b90cf1e22b45d7ddc3db9c8d5",
2331
+                "url": "https://api.github.com/repos/filamentphp/tables/zipball/6c183a98ee19f491f229dbca732f0b00ad9e31be",
2332
+                "reference": "6c183a98ee19f491f229dbca732f0b00ad9e31be",
2333 2333
                 "shasum": ""
2334 2334
             },
2335 2335
             "require": {
@@ -2369,11 +2369,11 @@
2369 2369
                 "issues": "https://github.com/filamentphp/filament/issues",
2370 2370
                 "source": "https://github.com/filamentphp/filament"
2371 2371
             },
2372
-            "time": "2024-05-20T16:26:45+00:00"
2372
+            "time": "2024-05-23T12:08:36+00:00"
2373 2373
         },
2374 2374
         {
2375 2375
             "name": "filament/widgets",
2376
-            "version": "v3.2.81",
2376
+            "version": "v3.2.82",
2377 2377
             "source": {
2378 2378
                 "type": "git",
2379 2379
                 "url": "https://github.com/filamentphp/widgets.git",
@@ -3874,40 +3874,39 @@
3874 3874
         },
3875 3875
         {
3876 3876
             "name": "league/csv",
3877
-            "version": "9.15.0",
3877
+            "version": "9.16.0",
3878 3878
             "source": {
3879 3879
                 "type": "git",
3880 3880
                 "url": "https://github.com/thephpleague/csv.git",
3881
-                "reference": "fa7e2441c0bc9b2360f4314fd6c954f7ff40d435"
3881
+                "reference": "998280c6c34bd67d8125fdc8b45bae28d761b440"
3882 3882
             },
3883 3883
             "dist": {
3884 3884
                 "type": "zip",
3885
-                "url": "https://api.github.com/repos/thephpleague/csv/zipball/fa7e2441c0bc9b2360f4314fd6c954f7ff40d435",
3886
-                "reference": "fa7e2441c0bc9b2360f4314fd6c954f7ff40d435",
3885
+                "url": "https://api.github.com/repos/thephpleague/csv/zipball/998280c6c34bd67d8125fdc8b45bae28d761b440",
3886
+                "reference": "998280c6c34bd67d8125fdc8b45bae28d761b440",
3887 3887
                 "shasum": ""
3888 3888
             },
3889 3889
             "require": {
3890 3890
                 "ext-filter": "*",
3891
-                "ext-json": "*",
3892
-                "ext-mbstring": "*",
3893 3891
                 "php": "^8.1.2"
3894 3892
             },
3895 3893
             "require-dev": {
3896
-                "doctrine/collections": "^2.1.4",
3894
+                "doctrine/collections": "^2.2.2",
3897 3895
                 "ext-dom": "*",
3898 3896
                 "ext-xdebug": "*",
3899
-                "friendsofphp/php-cs-fixer": "^v3.22.0",
3897
+                "friendsofphp/php-cs-fixer": "^3.57.1",
3900 3898
                 "phpbench/phpbench": "^1.2.15",
3901
-                "phpstan/phpstan": "^1.10.57",
3902
-                "phpstan/phpstan-deprecation-rules": "^1.1.4",
3903
-                "phpstan/phpstan-phpunit": "^1.3.15",
3904
-                "phpstan/phpstan-strict-rules": "^1.5.2",
3905
-                "phpunit/phpunit": "^10.5.9",
3906
-                "symfony/var-dumper": "^6.4.2"
3899
+                "phpstan/phpstan": "^1.11.1",
3900
+                "phpstan/phpstan-deprecation-rules": "^1.2.0",
3901
+                "phpstan/phpstan-phpunit": "^1.4.0",
3902
+                "phpstan/phpstan-strict-rules": "^1.6.0",
3903
+                "phpunit/phpunit": "^10.5.16 || ^11.1.3",
3904
+                "symfony/var-dumper": "^6.4.6 || ^7.0.7"
3907 3905
             },
3908 3906
             "suggest": {
3909 3907
                 "ext-dom": "Required to use the XMLConverter and the HTMLConverter classes",
3910
-                "ext-iconv": "Needed to ease transcoding CSV using iconv stream filters"
3908
+                "ext-iconv": "Needed to ease transcoding CSV using iconv stream filters",
3909
+                "ext-mbstring": "Needed to ease transcoding CSV using mb stream filters"
3911 3910
             },
3912 3911
             "type": "library",
3913 3912
             "extra": {
@@ -3959,7 +3958,7 @@
3959 3958
                     "type": "github"
3960 3959
                 }
3961 3960
             ],
3962
-            "time": "2024-02-20T20:00:00+00:00"
3961
+            "time": "2024-05-24T11:04:54+00:00"
3963 3962
         },
3964 3963
         {
3965 3964
             "name": "league/flysystem",

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

@@ -1079,9 +1079,9 @@
1079 1079
             "dev": true
1080 1080
         },
1081 1081
         "node_modules/electron-to-chromium": {
1082
-            "version": "1.4.778",
1083
-            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.778.tgz",
1084
-            "integrity": "sha512-C6q/xcUJf/2yODRxAVCfIk4j3y3LMsD0ehiE2RQNV2cxc8XU62gR6vvYh3+etSUzlgTfil+qDHI1vubpdf0TOA==",
1082
+            "version": "1.4.783",
1083
+            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.783.tgz",
1084
+            "integrity": "sha512-bT0jEz/Xz1fahQpbZ1D7LgmPYZ3iHVY39NcWWro1+hA2IvjiPeaXtfSqrQ+nXjApMvQRE2ASt1itSLRrebHMRQ==",
1085 1085
             "dev": true
1086 1086
         },
1087 1087
         "node_modules/emoji-regex": {
@@ -1273,16 +1273,16 @@
1273 1273
             }
1274 1274
         },
1275 1275
         "node_modules/glob": {
1276
-            "version": "10.3.16",
1277
-            "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.16.tgz",
1278
-            "integrity": "sha512-JDKXl1DiuuHJ6fVS2FXjownaavciiHNUU4mOvV/B793RLh05vZL1rcPnCSaOgv1hDT6RDlY7AB7ZUvFYAtPgAw==",
1276
+            "version": "10.4.1",
1277
+            "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz",
1278
+            "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==",
1279 1279
             "dev": true,
1280 1280
             "dependencies": {
1281 1281
                 "foreground-child": "^3.1.0",
1282 1282
                 "jackspeak": "^3.1.2",
1283
-                "minimatch": "^9.0.1",
1284
-                "minipass": "^7.0.4",
1285
-                "path-scurry": "^1.11.0"
1283
+                "minimatch": "^9.0.4",
1284
+                "minipass": "^7.1.2",
1285
+                "path-scurry": "^1.11.1"
1286 1286
             },
1287 1287
             "bin": {
1288 1288
                 "glob": "dist/esm/bin.mjs"
@@ -1543,9 +1543,9 @@
1543 1543
             }
1544 1544
         },
1545 1545
         "node_modules/minipass": {
1546
-            "version": "7.1.1",
1547
-            "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.1.tgz",
1548
-            "integrity": "sha512-UZ7eQ+h8ywIRAW1hIEl2AqdwzJucU/Kp59+8kkZeSvafXhZjul247BvIJjEVFVeON6d7lM46XX1HXCduKAS8VA==",
1546
+            "version": "7.1.2",
1547
+            "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
1548
+            "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
1549 1549
             "dev": true,
1550 1550
             "engines": {
1551 1551
                 "node": ">=16 || 14 >=14.17"
@@ -1833,9 +1833,9 @@
1833 1833
             }
1834 1834
         },
1835 1835
         "node_modules/postcss-nesting": {
1836
-            "version": "12.1.4",
1837
-            "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-12.1.4.tgz",
1838
-            "integrity": "sha512-CcHOq94K137E+U4Ommu7pexcpp0Tjm24zl4UcqWs1oSLAr5cLI+jLrqQ5h/bdjhMX6cMbzunyustVNnvrzF8Zg==",
1836
+            "version": "12.1.5",
1837
+            "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-12.1.5.tgz",
1838
+            "integrity": "sha512-N1NgI1PDCiAGWPTYrwqm8wpjv0bgDmkYHH72pNsqTCv9CObxjxftdYu6AKtGN+pnJa7FQjMm3v4sp8QJbFsYdQ==",
1839 1839
             "dev": true,
1840 1840
             "funding": [
1841 1841
                 {
@@ -1850,7 +1850,7 @@
1850 1850
             "dependencies": {
1851 1851
                 "@csstools/selector-resolve-nested": "^1.1.0",
1852 1852
                 "@csstools/selector-specificity": "^3.1.1",
1853
-                "postcss-selector-parser": "^6.0.13"
1853
+                "postcss-selector-parser": "^6.1.0"
1854 1854
             },
1855 1855
             "engines": {
1856 1856
                 "node": "^14 || ^16 || >=18"

+ 1
- 1
resources/views/filament/company/pages/reports/account-balances.blade.php 查看文件

@@ -2,7 +2,7 @@
2 2
     <div class="flex flex-col gap-y-6">
3 3
         <x-filament-tables::container>
4 4
             <div class="p-6 divide-y divide-gray-200 dark:divide-white/5">
5
-                <form wire:submit.prevent="loadAccountBalances" class="w-full">
5
+                <form wire:submit.prevent="loadReportData" class="w-full">
6 6
                     <div class="flex flex-col md:flex-row items-end justify-center gap-4 md:gap-6">
7 7
                         <div class="flex-grow">
8 8
                             {{ $this->form }}

+ 64
- 0
resources/views/filament/company/pages/reports/trial-balance.blade.php 查看文件

@@ -0,0 +1,64 @@
1
+<x-filament-panels::page>
2
+    <div class="flex flex-col gap-y-6">
3
+        <x-filament-tables::container>
4
+            <div class="p-6 divide-y divide-gray-200 dark:divide-white/5">
5
+                <form wire:submit.prevent="loadReportData" class="w-full">
6
+                    <div class="flex flex-col md:flex-row items-end justify-center gap-4 md:gap-6">
7
+                        <div class="flex-grow">
8
+                            {{ $this->form }}
9
+                        </div>
10
+                        <x-filament::button type="submit" class="mt-4 md:mt-0">
11
+                            Update Report
12
+                        </x-filament::button>
13
+                    </div>
14
+                </form>
15
+            </div>
16
+            <div class="divide-y divide-gray-200 overflow-x-auto dark:divide-white/10 dark:border-t-white/10">
17
+                <table class="w-full table-auto divide-y divide-gray-200 text-start dark:divide-white/5">
18
+                    <thead class="divide-y divide-gray-200 dark:divide-white/5">
19
+                        <tr class="bg-gray-50 dark:bg-white/5">
20
+                            <x-filament-tables::header-cell>Account</x-filament-tables::header-cell>
21
+                            <x-filament-tables::header-cell alignment="end">Debit</x-filament-tables::header-cell>
22
+                            <x-filament-tables::header-cell alignment="end">Credit</x-filament-tables::header-cell>
23
+                        </tr>
24
+                    </thead>
25
+                    @foreach($trialBalanceReport->categories as $accountCategoryName => $accountCategory)
26
+                        <tbody class="divide-y divide-gray-200 whitespace-nowrap dark:divide-white/5">
27
+                        <tr class="bg-gray-50 dark:bg-white/5">
28
+                            <x-filament-tables::cell colspan="3">
29
+                                <div class="px-3 py-2 text-sm font-medium text-gray-950 dark:text-white">{{ $accountCategoryName }}</div>
30
+                            </x-filament-tables::cell>
31
+                        </tr>
32
+                        @foreach($accountCategory->accounts as $account)
33
+                            <x-filament-tables::row>
34
+                                <x-filament-tables::cell><div class="px-3 py-4 text-sm leading-6 text-gray-950 dark:text-white">{{ $account->accountName }}</div></x-filament-tables::cell>
35
+                                <x-filament-tables::cell class="text-right"><div class="px-3 py-4 text-sm leading-6 text-gray-950 dark:text-white">{{ $account->balance->debitBalance ?? '' }}</div></x-filament-tables::cell>
36
+                                <x-filament-tables::cell class="text-right"><div class="px-3 py-4 text-sm leading-6 text-gray-950 dark:text-white">{{ $account->balance->creditBalance ?? '' }}</div></x-filament-tables::cell>
37
+                            </x-filament-tables::row>
38
+                        @endforeach
39
+                        <x-filament-tables::row>
40
+                            <x-filament-tables::cell><div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white">Total {{ $accountCategoryName }}</div></x-filament-tables::cell>
41
+                            <x-filament-tables::cell class="text-right"><div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white">{{ $accountCategory->summary->debitBalance }}</div></x-filament-tables::cell>
42
+                            <x-filament-tables::cell class="text-right"><div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white">{{ $accountCategory->summary->creditBalance }}</div></x-filament-tables::cell>
43
+                        </x-filament-tables::row>
44
+                        <x-filament-tables::row>
45
+                            <x-filament-tables::cell colspan="3">
46
+                                <div class="px-3 py-2 invisible">Hidden Text</div>
47
+                            </x-filament-tables::cell>
48
+                        </x-filament-tables::row>
49
+                        </tbody>
50
+                    @endforeach
51
+                    <tfoot>
52
+                        <tr class="bg-gray-50 dark:bg-white/5">
53
+                            <x-filament-tables::cell><div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white">Total for all accounts</div></x-filament-tables::cell>
54
+                            <x-filament-tables::cell class="text-right"><div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white">{{ $trialBalanceReport->overallTotal->debitBalance }}</div></x-filament-tables::cell>
55
+                            <x-filament-tables::cell class="text-right"><div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white">{{ $trialBalanceReport->overallTotal->creditBalance }}</div></x-filament-tables::cell>
56
+                        </tr>
57
+                    </tfoot>
58
+                </table>
59
+            </div>
60
+            <div class="es-table__footer-ctn border-t border-gray-200"></div>
61
+        </x-filament-tables::container>
62
+    </div>
63
+</x-filament-panels::page>
64
+

正在加载...
取消
保存