Browse Source

Trial Balance report and refactor

3.x
wallo 1 year ago
parent
commit
0175e82eb9
33 changed files with 567 additions and 477 deletions
  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 View File

2
 
2
 
3
 namespace App\Contracts;
3
 namespace App\Contracts;
4
 
4
 
5
-use App\DTO\AccountBalanceDTO;
6
-use App\DTO\AccountBalanceReportDTO;
7
-use App\Enums\Accounting\AccountCategory;
8
 use App\Models\Accounting\Account;
5
 use App\Models\Accounting\Account;
9
 use App\ValueObjects\Money;
6
 use App\ValueObjects\Money;
10
 
7
 
20
 
17
 
21
     public function getEndingBalance(Account $account, string $startDate, string $endDate): ?Money;
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
     public function getBalances(Account $account, string $startDate, string $endDate): array;
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
     public function getTotalBalanceForAllBankAccounts(string $startDate, string $endDate): Money;
22
     public function getTotalBalanceForAllBankAccounts(string $startDate, string $endDate): Money;
32
 
23
 
33
     public function getAccountCategoryOrder(): array;
24
     public function getAccountCategoryOrder(): array;

app/DTO/AccountBalanceReportDTO.php → app/DTO/ReportDTO.php View File

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

+ 3
- 8
app/Events/CompanyConfigured.php View File

13
     use InteractsWithSockets;
13
     use InteractsWithSockets;
14
     use SerializesModels;
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 View File

13
     use InteractsWithSockets;
13
     use InteractsWithSockets;
14
     use SerializesModels;
14
     use SerializesModels;
15
 
15
 
16
-    public Model $model;
17
-
18
     /**
16
     /**
19
      * Create a new event instance.
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 View File

13
     use InteractsWithSockets;
13
     use InteractsWithSockets;
14
     use SerializesModels;
14
     use SerializesModels;
15
 
15
 
16
-    public Model $record;
17
-
18
-    public array $data;
19
-
20
     /**
16
     /**
21
      * Create a new event instance.
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 View File

12
     use Dispatchable;
12
     use Dispatchable;
13
     use SerializesModels;
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
      * Create a new event instance.
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 View File

13
     use InteractsWithSockets;
13
     use InteractsWithSockets;
14
     use SerializesModels;
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 View File

13
     use InteractsWithSockets;
13
     use InteractsWithSockets;
14
     use SerializesModels;
14
     use SerializesModels;
15
 
15
 
16
-    public Currency $currency;
17
-
18
     /**
16
     /**
19
      * Create a new event instance.
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 View File

11
     use Dispatchable;
11
     use Dispatchable;
12
     use SerializesModels;
12
     use SerializesModels;
13
 
13
 
14
-    public string $publicToken;
15
-
16
-    public string $accessToken;
17
-
18
-    public Company $company;
19
-
20
     /**
14
     /**
21
      * Create a new event instance.
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 View File

12
     use Dispatchable;
12
     use Dispatchable;
13
     use SerializesModels;
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
      * Create a new event instance.
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 View File

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

+ 2
- 1
app/Filament/Company/Pages/Reports.php View File

3
 namespace App\Filament\Company\Pages;
3
 namespace App\Filament\Company\Pages;
4
 
4
 
5
 use App\Filament\Company\Pages\Reports\AccountBalances;
5
 use App\Filament\Company\Pages\Reports\AccountBalances;
6
+use App\Filament\Company\Pages\Reports\TrialBalance;
6
 use App\Infolists\Components\ReportEntry;
7
 use App\Infolists\Components\ReportEntry;
7
 use Filament\Infolists\Components\Section;
8
 use Filament\Infolists\Components\Section;
8
 use Filament\Infolists\Infolist;
9
 use Filament\Infolists\Infolist;
38
                             ->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
                             ->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
                             ->icon('heroicon-o-scale')
40
                             ->icon('heroicon-o-scale')
40
                             ->iconColor(Color::Sky)
41
                             ->iconColor(Color::Sky)
41
-                            ->url('#'),
42
+                            ->url(TrialBalance::getUrl()),
42
                         ReportEntry::make('account_transactions')
43
                         ReportEntry::make('account_transactions')
43
                             ->hiddenLabel()
44
                             ->hiddenLabel()
44
                             ->heading('Account Transactions')
45
                             ->heading('Account Transactions')

+ 10
- 80
app/Filament/Company/Pages/Reports/AccountBalances.php View File

2
 
2
 
3
 namespace App\Filament\Company\Pages\Reports;
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
 use App\Services\AccountBalancesExportService;
6
 use App\Services\AccountBalancesExportService;
9
-use App\Services\AccountService;
7
+use App\Services\ReportService;
10
 use Barryvdh\DomPDF\Facade\Pdf;
8
 use Barryvdh\DomPDF\Facade\Pdf;
11
 use Filament\Actions\Action;
9
 use Filament\Actions\Action;
12
 use Filament\Actions\ActionGroup;
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
 use Filament\Support\Enums\IconPosition;
11
 use Filament\Support\Enums\IconPosition;
19
 use Filament\Support\Enums\IconSize;
12
 use Filament\Support\Enums\IconSize;
20
 use Illuminate\Support\Carbon;
13
 use Illuminate\Support\Carbon;
21
 use Symfony\Component\HttpFoundation\StreamedResponse;
14
 use Symfony\Component\HttpFoundation\StreamedResponse;
22
 
15
 
23
-class AccountBalances extends Page
16
+class AccountBalances extends BaseReportPage
24
 {
17
 {
25
     protected static string $view = 'filament.company.pages.reports.account-balances';
18
     protected static string $view = 'filament.company.pages.reports.account-balances';
26
 
19
 
27
     protected static ?string $slug = 'reports/account-balances';
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
     protected AccountBalancesExportService $accountBalancesExportService;
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
         $this->accountBalancesExportService = $accountBalancesExportService;
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
     protected function getHeaderActions(): array
41
     protected function getHeaderActions(): array
110
             echo $pdf->stream();
77
             echo $pdf->stream();
111
         }, 'account-balances.pdf');
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 View File

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 View File

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 View File

29
     private function handleDefaultUpdate(array $payload): void
29
     private function handleDefaultUpdate(array $payload): void
30
     {
30
     {
31
         $newTransactions = $payload['new_transactions'];
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
         })->first();
36
         })->first();
37
 
37
 
38
         if ($company && $newTransactions > 0) {
38
         if ($company && $newTransactions > 0) {
39
-            ProcessTransactionUpdate::dispatch($company, $itemID)
39
+            ProcessTransactionUpdate::dispatch($company, $itemId)
40
                 ->onQueue('transactions');
40
                 ->onQueue('transactions');
41
         }
41
         }
42
     }
42
     }

+ 7
- 20
app/Jobs/ProcessTransactionImport.php View File

22
     use Queueable;
22
     use Queueable;
23
     use SerializesModels;
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 View File

20
     use Queueable;
20
     use Queueable;
21
     use SerializesModels;
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
     public function handle(PlaidService $plaidService, TransactionService $transactionService): void
32
     public function handle(PlaidService $plaidService, TransactionService $transactionService): void
40
     {
33
     {
41
         $connectedBankAccounts = $this->company->connectedBankAccounts()
34
         $connectedBankAccounts = $this->company->connectedBankAccounts()
42
-            ->where('item_id', $this->item_id)
35
+            ->where('item_id', $this->itemId)
43
             ->where('import_transactions', true)
36
             ->where('import_transactions', true)
44
             ->get();
37
             ->get();
45
 
38
 

+ 5
- 7
app/Listeners/CreateConnectedAccount.php View File

10
 
10
 
11
 class CreateConnectedAccount
11
 class CreateConnectedAccount
12
 {
12
 {
13
-    protected PlaidService $plaid;
14
-
15
     /**
13
     /**
16
      * Create the event listener.
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
 
34
 
37
         $company = $event->company;
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
         $this->processInstitution($authResponse, $institutionResponse, $company, $accessToken);
41
         $this->processInstitution($authResponse, $institutionResponse, $company, $accessToken);
44
     }
42
     }

+ 3
- 5
app/Listeners/HandleTransactionImport.php View File

9
 
9
 
10
 class HandleTransactionImport
10
 class HandleTransactionImport
11
 {
11
 {
12
-    protected ConnectedBankAccountService $connectedBankAccountService;
13
-
14
     /**
12
     /**
15
      * Create the event listener.
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 View File

13
     /**
13
     /**
14
      * Create the event listener.
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 View File

2
 
2
 
3
 namespace App\Services;
3
 namespace App\Services;
4
 
4
 
5
-use App\DTO\AccountBalanceReportDTO;
5
+use App\DTO\ReportDTO;
6
 use App\Models\Company;
6
 use App\Models\Company;
7
 use Symfony\Component\HttpFoundation\StreamedResponse;
7
 use Symfony\Component\HttpFoundation\StreamedResponse;
8
 
8
 
9
 class AccountBalancesExportService
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
         // Construct the filename
13
         // Construct the filename
14
         $filename = $company->name . ' Account Balances ' . $startDate . ' to ' . $endDate . '.csv';
14
         $filename = $company->name . ' Account Balances ' . $startDate . ' to ' . $endDate . '.csv';

+ 7
- 99
app/Services/AccountService.php View File

3
 namespace App\Services;
3
 namespace App\Services;
4
 
4
 
5
 use App\Contracts\AccountHandler;
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
 use App\Enums\Accounting\AccountCategory;
6
 use App\Enums\Accounting\AccountCategory;
11
 use App\Models\Accounting\Account;
7
 use App\Models\Accounting\Account;
12
 use App\Models\Accounting\Transaction;
8
 use App\Models\Accounting\Transaction;
14
 use App\Repositories\Accounting\JournalEntryRepository;
10
 use App\Repositories\Accounting\JournalEntryRepository;
15
 use App\Utilities\Currency\CurrencyAccessor;
11
 use App\Utilities\Currency\CurrencyAccessor;
16
 use App\ValueObjects\Money;
12
 use App\ValueObjects\Money;
17
-use Illuminate\Database\Eloquent\Collection;
18
 
13
 
19
 class AccountService implements AccountHandler
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
     public function getDebitBalance(Account $account, string $startDate, string $endDate): Money
21
     public function getDebitBalance(Account $account, string $startDate, string $endDate): Money
63
 
56
 
64
     public function getEndingBalance(Account $account, string $startDate, string $endDate): ?Money
57
     public function getEndingBalance(Account $account, string $startDate, string $endDate): ?Money
65
     {
58
     {
59
+        $netMovement = $this->getNetMovement($account, $startDate, $endDate)->getAmount();
60
+
66
         if (in_array($account->category, [AccountCategory::Expense, AccountCategory::Revenue], true)) {
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
         $startingBalance = $this->getStartingBalance($account, $startDate)?->getAmount();
65
         $startingBalance = $this->getStartingBalance($account, $startDate)?->getAmount();
71
-        $netMovement = $this->getNetMovement($account, $startDate, $endDate)->getAmount();
72
         $endingBalance = $startingBalance + $netMovement;
66
         $endingBalance = $startingBalance + $netMovement;
73
 
67
 
74
         return new Money($endingBalance, $account->currency_code);
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
         return match ($category) {
73
         return match ($category) {
80
             AccountCategory::Asset, AccountCategory::Expense => $debitBalance - $creditBalance,
74
             AccountCategory::Asset, AccountCategory::Expense => $debitBalance - $creditBalance,
102
         return $balances;
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
     public function getTotalBalanceForAllBankAccounts(string $startDate, string $endDate): Money
99
     public function getTotalBalanceForAllBankAccounts(string $startDate, string $endDate): Money
191
     {
100
     {
192
         $bankAccounts = BankAccount::with('account')
101
         $bankAccounts = BankAccount::with('account')
194
 
103
 
195
         $totalBalance = 0;
104
         $totalBalance = 0;
196
 
105
 
197
-        // Get ending balance for each bank account
198
         foreach ($bankAccounts as $bankAccount) {
106
         foreach ($bankAccounts as $bankAccount) {
199
             $account = $bankAccount->account;
107
             $account = $bankAccount->account;
200
 
108
 

+ 4
- 8
app/Services/ConnectedBankAccountService.php View File

11
 
11
 
12
 class ConnectedBankAccountService
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
     public function getOrProcessBankAccountForConnectedBankAccount(Company $company, ConnectedBankAccount $connectedBankAccount, int | string $selectedBankAccountId): BankAccount
20
     public function getOrProcessBankAccountForConnectedBankAccount(Company $company, ConnectedBankAccount $connectedBankAccount, int | string $selectedBankAccountId): BankAccount

+ 8
- 14
app/Services/CurrencyService.php View File

10
 
10
 
11
 class CurrencyService implements CurrencyHandler
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
      */
22
      */
29
     public function isEnabled(): bool
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
     public function getSupportedCurrencies(): ?array
28
     public function getSupportedCurrencies(): ?array
38
         }
32
         }
39
 
33
 
40
         return Cache::remember('supported_currency_codes', now()->addMonth(), function () {
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
             if ($response->getStatusCode() === 200) {
37
             if ($response->getStatusCode() === 200) {
44
                 $responseData = json_decode($response->getBody()->getContents(), true);
38
                 $responseData = json_decode($response->getBody()->getContents(), true);
106
     public function updateCurrencyRatesCache(string $baseCurrency): ?array
100
     public function updateCurrencyRatesCache(string $baseCurrency): ?array
107
     {
101
     {
108
         try {
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
             if ($response->getStatusCode() === 200) {
105
             if ($response->getStatusCode() === 200) {
112
                 $responseData = json_decode($response->getBody()->getContents(), true);
106
                 $responseData = json_decode($response->getBody()->getContents(), true);

+ 47
- 37
app/Services/PlaidService.php View File

15
 {
15
 {
16
     public const API_VERSION = '2020-09-14';
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
     protected ?string $environment;
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
     protected HttpClient $client;
28
     protected HttpClient $client;
29
 
29
 
50
     {
50
     {
51
         $this->client = $client;
51
         $this->client = $client;
52
         $this->config = $config;
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
         $this->environment = $this->config->get('plaid.environment', 'sandbox');
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
         $this->setBaseUrl($this->environment);
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
         return $this;
66
         return $this;
67
     }
67
     }
77
 
77
 
78
     public function setBaseUrl(?string $environment): void
78
     public function setBaseUrl(?string $environment): void
79
     {
79
     {
80
-        $this->base_url = match ($environment) {
80
+        $this->baseUrl = match ($environment) {
81
             'development' => 'https://development.plaid.com',
81
             'development' => 'https://development.plaid.com',
82
             'production' => 'https://production.plaid.com',
82
             'production' => 'https://production.plaid.com',
83
             default => 'https://sandbox.plaid.com', // Default to sandbox, including if environment is null
83
             default => 'https://sandbox.plaid.com', // Default to sandbox, including if environment is null
86
 
86
 
87
     public function getBaseUrl(): string
87
     public function getBaseUrl(): string
88
     {
88
     {
89
-        return $this->base_url;
89
+        return $this->baseUrl;
90
     }
90
     }
91
 
91
 
92
     public function getEnvironment(): string
92
     public function getEnvironment(): string
99
         $request = $this->client->withHeaders([
99
         $request = $this->client->withHeaders([
100
             'Plaid-Version' => self::API_VERSION,
100
             'Plaid-Version' => self::API_VERSION,
101
             'Content-Type' => 'application/json',
101
             'Content-Type' => 'application/json',
102
-        ])->baseUrl($this->base_url);
102
+        ])->baseUrl($this->baseUrl);
103
 
103
 
104
         if ($method === 'post') {
104
         if ($method === 'post') {
105
             $request = $request->withHeaders([
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
         );
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
         $data = [
186
         $data = [
187
-            'client_name' => $client_name,
187
+            'client_name' => $clientName,
188
             'language' => $language,
188
             'language' => $language,
189
-            'country_codes' => $country_codes,
189
+            'country_codes' => $countryCodes,
190
             'user' => (object) $user,
190
             'user' => (object) $user,
191
         ];
191
         ];
192
 
192
 
194
             $data['products'] = $products;
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
         return $this->sendRequest('link/token/create', $data);
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
         return $this->sendRequest('item/public_token/exchange', $data);
210
         return $this->sendRequest('item/public_token/exchange', $data);
209
     }
211
     }
218
         return $this->sendRequest('accounts/get', $data);
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
         $options = [
225
         $options = [
224
             'include_optional_metadata' => true,
226
             'include_optional_metadata' => true,
226
 
228
 
227
         $plaidCountry = $this->getCountry($country);
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
         $data = [
236
         $data = [
235
-            'institution_id' => $institution_id,
236
-            'country_codes' => $country_codes,
237
+            'institution_id' => $institutionId,
238
+            'country_codes' => $countryCodes,
237
             'options' => (object) $options,
239
             'options' => (object) $options,
238
         ];
240
         ];
239
 
241
 
240
         return $this->sendRequest('institutions/get_by_id', $data);
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
         $data = [
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
             'options' => (object) $options,
251
             'options' => (object) $options,
250
         ];
252
         ];
251
 
253
 
252
         return $this->sendRequest('transactions/get', $data);
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
         return $this->sendRequest('sandbox/item/fire_webhook', $data);
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
         return $this->sendRequest('transactions/refresh', $data);
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
         return $this->sendRequest('item/remove', $data);
283
         return $this->sendRequest('item/remove', $data);
274
     }
284
     }

+ 170
- 0
app/Services/ReportService.php View File

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 View File

7
 
7
 
8
 class Money
8
 class Money
9
 {
9
 {
10
-    private int $amount;
11
-
12
-    private string $currencyCode;
13
-
14
     private ?int $convertedAmount = null;
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
     public function getAmount(): int
18
     public function getAmount(): int

+ 4
- 8
app/View/Models/InvoiceViewModel.php View File

11
 {
11
 {
12
     use HasFont;
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
     public function logo(): ?string
20
     public function logo(): ?string

+ 53
- 54
composer.lock View File

497
         },
497
         },
498
         {
498
         {
499
             "name": "aws/aws-sdk-php",
499
             "name": "aws/aws-sdk-php",
500
-            "version": "3.308.1",
500
+            "version": "3.308.3",
501
             "source": {
501
             "source": {
502
                 "type": "git",
502
                 "type": "git",
503
                 "url": "https://github.com/aws/aws-sdk-php.git",
503
                 "url": "https://github.com/aws/aws-sdk-php.git",
504
-                "reference": "bf5f1221d4c5c67d3213150fb91dfb5ce627227b"
504
+                "reference": "7fa0625056fa1fcf6732f89ba37b3f630d78de59"
505
             },
505
             },
506
             "dist": {
506
             "dist": {
507
                 "type": "zip",
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
                 "shasum": ""
510
                 "shasum": ""
511
             },
511
             },
512
             "require": {
512
             "require": {
586
             "support": {
586
             "support": {
587
                 "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
587
                 "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
588
                 "issues": "https://github.com/aws/aws-sdk-php/issues",
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
             "name": "aws/aws-sdk-php-laravel",
594
             "name": "aws/aws-sdk-php-laravel",
1985
         },
1985
         },
1986
         {
1986
         {
1987
             "name": "filament/actions",
1987
             "name": "filament/actions",
1988
-            "version": "v3.2.81",
1988
+            "version": "v3.2.82",
1989
             "source": {
1989
             "source": {
1990
                 "type": "git",
1990
                 "type": "git",
1991
                 "url": "https://github.com/filamentphp/actions.git",
1991
                 "url": "https://github.com/filamentphp/actions.git",
1992
-                "reference": "05e19bf192dfc7eb13989543c4fae4c41ee08124"
1992
+                "reference": "889cb3538090cfe0eef1ca51ce7808646133b7ed"
1993
             },
1993
             },
1994
             "dist": {
1994
             "dist": {
1995
                 "type": "zip",
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
                 "shasum": ""
1998
                 "shasum": ""
1999
             },
1999
             },
2000
             "require": {
2000
             "require": {
2034
                 "issues": "https://github.com/filamentphp/filament/issues",
2034
                 "issues": "https://github.com/filamentphp/filament/issues",
2035
                 "source": "https://github.com/filamentphp/filament"
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
             "name": "filament/filament",
2040
             "name": "filament/filament",
2041
-            "version": "v3.2.81",
2041
+            "version": "v3.2.82",
2042
             "source": {
2042
             "source": {
2043
                 "type": "git",
2043
                 "type": "git",
2044
                 "url": "https://github.com/filamentphp/panels.git",
2044
                 "url": "https://github.com/filamentphp/panels.git",
2045
-                "reference": "3fb5cbeb32b02ef196a750441c7ac452c273efab"
2045
+                "reference": "b73e49d6ef02dba8f118d2934435748ed4feffa2"
2046
             },
2046
             },
2047
             "dist": {
2047
             "dist": {
2048
                 "type": "zip",
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
                 "shasum": ""
2051
                 "shasum": ""
2052
             },
2052
             },
2053
             "require": {
2053
             "require": {
2099
                 "issues": "https://github.com/filamentphp/filament/issues",
2099
                 "issues": "https://github.com/filamentphp/filament/issues",
2100
                 "source": "https://github.com/filamentphp/filament"
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
             "name": "filament/forms",
2105
             "name": "filament/forms",
2106
-            "version": "v3.2.81",
2106
+            "version": "v3.2.82",
2107
             "source": {
2107
             "source": {
2108
                 "type": "git",
2108
                 "type": "git",
2109
                 "url": "https://github.com/filamentphp/forms.git",
2109
                 "url": "https://github.com/filamentphp/forms.git",
2110
-                "reference": "9ab84e46226acdbbf48e1f73fa1d141d566eab73"
2110
+                "reference": "c97fb3c5a6e5dd0d4d9cdbaa8b43c99c0cc35a01"
2111
             },
2111
             },
2112
             "dist": {
2112
             "dist": {
2113
                 "type": "zip",
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
                 "shasum": ""
2116
                 "shasum": ""
2117
             },
2117
             },
2118
             "require": {
2118
             "require": {
2155
                 "issues": "https://github.com/filamentphp/filament/issues",
2155
                 "issues": "https://github.com/filamentphp/filament/issues",
2156
                 "source": "https://github.com/filamentphp/filament"
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
             "name": "filament/infolists",
2161
             "name": "filament/infolists",
2162
-            "version": "v3.2.81",
2162
+            "version": "v3.2.82",
2163
             "source": {
2163
             "source": {
2164
                 "type": "git",
2164
                 "type": "git",
2165
                 "url": "https://github.com/filamentphp/infolists.git",
2165
                 "url": "https://github.com/filamentphp/infolists.git",
2166
-                "reference": "214c92c446277bb6599758a29ad885e79b11e6b0"
2166
+                "reference": "d473d1b11ad32c917cf6e3610411fd790b8c1e27"
2167
             },
2167
             },
2168
             "dist": {
2168
             "dist": {
2169
                 "type": "zip",
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
                 "shasum": ""
2172
                 "shasum": ""
2173
             },
2173
             },
2174
             "require": {
2174
             "require": {
2206
                 "issues": "https://github.com/filamentphp/filament/issues",
2206
                 "issues": "https://github.com/filamentphp/filament/issues",
2207
                 "source": "https://github.com/filamentphp/filament"
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
             "name": "filament/notifications",
2212
             "name": "filament/notifications",
2213
-            "version": "v3.2.81",
2213
+            "version": "v3.2.82",
2214
             "source": {
2214
             "source": {
2215
                 "type": "git",
2215
                 "type": "git",
2216
                 "url": "https://github.com/filamentphp/notifications.git",
2216
                 "url": "https://github.com/filamentphp/notifications.git",
2262
         },
2262
         },
2263
         {
2263
         {
2264
             "name": "filament/support",
2264
             "name": "filament/support",
2265
-            "version": "v3.2.81",
2265
+            "version": "v3.2.82",
2266
             "source": {
2266
             "source": {
2267
                 "type": "git",
2267
                 "type": "git",
2268
                 "url": "https://github.com/filamentphp/support.git",
2268
                 "url": "https://github.com/filamentphp/support.git",
2269
-                "reference": "1435caac4c2a454ab0eb11fc6590628eda36aa0b"
2269
+                "reference": "f32b841309d01c4538d8f128d67ec213969747e9"
2270
             },
2270
             },
2271
             "dist": {
2271
             "dist": {
2272
                 "type": "zip",
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
                 "shasum": ""
2275
                 "shasum": ""
2276
             },
2276
             },
2277
             "require": {
2277
             "require": {
2316
                 "issues": "https://github.com/filamentphp/filament/issues",
2316
                 "issues": "https://github.com/filamentphp/filament/issues",
2317
                 "source": "https://github.com/filamentphp/filament"
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
             "name": "filament/tables",
2322
             "name": "filament/tables",
2323
-            "version": "v3.2.81",
2323
+            "version": "v3.2.82",
2324
             "source": {
2324
             "source": {
2325
                 "type": "git",
2325
                 "type": "git",
2326
                 "url": "https://github.com/filamentphp/tables.git",
2326
                 "url": "https://github.com/filamentphp/tables.git",
2327
-                "reference": "9f0141c7076c096b90cf1e22b45d7ddc3db9c8d5"
2327
+                "reference": "6c183a98ee19f491f229dbca732f0b00ad9e31be"
2328
             },
2328
             },
2329
             "dist": {
2329
             "dist": {
2330
                 "type": "zip",
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
                 "shasum": ""
2333
                 "shasum": ""
2334
             },
2334
             },
2335
             "require": {
2335
             "require": {
2369
                 "issues": "https://github.com/filamentphp/filament/issues",
2369
                 "issues": "https://github.com/filamentphp/filament/issues",
2370
                 "source": "https://github.com/filamentphp/filament"
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
             "name": "filament/widgets",
2375
             "name": "filament/widgets",
2376
-            "version": "v3.2.81",
2376
+            "version": "v3.2.82",
2377
             "source": {
2377
             "source": {
2378
                 "type": "git",
2378
                 "type": "git",
2379
                 "url": "https://github.com/filamentphp/widgets.git",
2379
                 "url": "https://github.com/filamentphp/widgets.git",
3874
         },
3874
         },
3875
         {
3875
         {
3876
             "name": "league/csv",
3876
             "name": "league/csv",
3877
-            "version": "9.15.0",
3877
+            "version": "9.16.0",
3878
             "source": {
3878
             "source": {
3879
                 "type": "git",
3879
                 "type": "git",
3880
                 "url": "https://github.com/thephpleague/csv.git",
3880
                 "url": "https://github.com/thephpleague/csv.git",
3881
-                "reference": "fa7e2441c0bc9b2360f4314fd6c954f7ff40d435"
3881
+                "reference": "998280c6c34bd67d8125fdc8b45bae28d761b440"
3882
             },
3882
             },
3883
             "dist": {
3883
             "dist": {
3884
                 "type": "zip",
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
                 "shasum": ""
3887
                 "shasum": ""
3888
             },
3888
             },
3889
             "require": {
3889
             "require": {
3890
                 "ext-filter": "*",
3890
                 "ext-filter": "*",
3891
-                "ext-json": "*",
3892
-                "ext-mbstring": "*",
3893
                 "php": "^8.1.2"
3891
                 "php": "^8.1.2"
3894
             },
3892
             },
3895
             "require-dev": {
3893
             "require-dev": {
3896
-                "doctrine/collections": "^2.1.4",
3894
+                "doctrine/collections": "^2.2.2",
3897
                 "ext-dom": "*",
3895
                 "ext-dom": "*",
3898
                 "ext-xdebug": "*",
3896
                 "ext-xdebug": "*",
3899
-                "friendsofphp/php-cs-fixer": "^v3.22.0",
3897
+                "friendsofphp/php-cs-fixer": "^3.57.1",
3900
                 "phpbench/phpbench": "^1.2.15",
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
             "suggest": {
3906
             "suggest": {
3909
                 "ext-dom": "Required to use the XMLConverter and the HTMLConverter classes",
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
             "type": "library",
3911
             "type": "library",
3913
             "extra": {
3912
             "extra": {
3959
                     "type": "github"
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
             "name": "league/flysystem",
3964
             "name": "league/flysystem",

+ 16
- 16
package-lock.json View File

1079
             "dev": true
1079
             "dev": true
1080
         },
1080
         },
1081
         "node_modules/electron-to-chromium": {
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
             "dev": true
1085
             "dev": true
1086
         },
1086
         },
1087
         "node_modules/emoji-regex": {
1087
         "node_modules/emoji-regex": {
1273
             }
1273
             }
1274
         },
1274
         },
1275
         "node_modules/glob": {
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
             "dev": true,
1279
             "dev": true,
1280
             "dependencies": {
1280
             "dependencies": {
1281
                 "foreground-child": "^3.1.0",
1281
                 "foreground-child": "^3.1.0",
1282
                 "jackspeak": "^3.1.2",
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
             "bin": {
1287
             "bin": {
1288
                 "glob": "dist/esm/bin.mjs"
1288
                 "glob": "dist/esm/bin.mjs"
1543
             }
1543
             }
1544
         },
1544
         },
1545
         "node_modules/minipass": {
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
             "dev": true,
1549
             "dev": true,
1550
             "engines": {
1550
             "engines": {
1551
                 "node": ">=16 || 14 >=14.17"
1551
                 "node": ">=16 || 14 >=14.17"
1833
             }
1833
             }
1834
         },
1834
         },
1835
         "node_modules/postcss-nesting": {
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
             "dev": true,
1839
             "dev": true,
1840
             "funding": [
1840
             "funding": [
1841
                 {
1841
                 {
1850
             "dependencies": {
1850
             "dependencies": {
1851
                 "@csstools/selector-resolve-nested": "^1.1.0",
1851
                 "@csstools/selector-resolve-nested": "^1.1.0",
1852
                 "@csstools/selector-specificity": "^3.1.1",
1852
                 "@csstools/selector-specificity": "^3.1.1",
1853
-                "postcss-selector-parser": "^6.0.13"
1853
+                "postcss-selector-parser": "^6.1.0"
1854
             },
1854
             },
1855
             "engines": {
1855
             "engines": {
1856
                 "node": "^14 || ^16 || >=18"
1856
                 "node": "^14 || ^16 || >=18"

+ 1
- 1
resources/views/filament/company/pages/reports/account-balances.blade.php View File

2
     <div class="flex flex-col gap-y-6">
2
     <div class="flex flex-col gap-y-6">
3
         <x-filament-tables::container>
3
         <x-filament-tables::container>
4
             <div class="p-6 divide-y divide-gray-200 dark:divide-white/5">
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
                     <div class="flex flex-col md:flex-row items-end justify-center gap-4 md:gap-6">
6
                     <div class="flex flex-col md:flex-row items-end justify-center gap-4 md:gap-6">
7
                         <div class="flex-grow">
7
                         <div class="flex-grow">
8
                             {{ $this->form }}
8
                             {{ $this->form }}

+ 64
- 0
resources/views/filament/company/pages/reports/trial-balance.blade.php View File

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
+

Loading…
Cancel
Save