瀏覽代碼

update: Plaid transaction import

3.x
wallo 1 年之前
父節點
當前提交
89f45d8339

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

16
 
16
 
17
     public ConnectedBankAccount $connectedBankAccount;
17
     public ConnectedBankAccount $connectedBankAccount;
18
 
18
 
19
-    public mixed $selectedBankAccountId;
19
+    public int | string $selectedBankAccountId;
20
 
20
 
21
-    public mixed $startDate;
21
+    public string $startDate;
22
 
22
 
23
     /**
23
     /**
24
      * Create a new event instance.
24
      * Create a new event instance.
25
      */
25
      */
26
-    public function __construct($company, $connectedBankAccount, $selectedBankAccountId, $startDate)
26
+    public function __construct(Company $company, ConnectedBankAccount $connectedBankAccount, int | string $selectedBankAccountId, string $startDate)
27
     {
27
     {
28
         $this->company = $company;
28
         $this->company = $company;
29
         $this->connectedBankAccount = $connectedBankAccount;
29
         $this->connectedBankAccount = $connectedBankAccount;

+ 6
- 2
app/Filament/Company/Resources/Accounting/TransactionResource.php 查看文件

80
 
80
 
81
                         return Carbon::parse($state)->translatedFormat($dateFormat);
81
                         return Carbon::parse($state)->translatedFormat($dateFormat);
82
                     }),
82
                     }),
83
+                Tables\Columns\TextColumn::make('bankAccount.account.name')
84
+                    ->label('Account')
85
+                    ->sortable(),
83
                 Tables\Columns\TextColumn::make('description')
86
                 Tables\Columns\TextColumn::make('description')
84
-                    ->wrap()
87
+                    ->limit(50)
85
                     ->label('Description'),
88
                     ->label('Description'),
86
                 Tables\Columns\TextColumn::make('category.name')
89
                 Tables\Columns\TextColumn::make('category.name')
87
                     ->label('Category')
90
                     ->label('Category')
88
                     ->html()
91
                     ->html()
89
-                    ->formatStateUsing(function ($state, Transaction $record) {
92
+                    ->formatStateUsing(static function ($state, Transaction $record) {
90
                         $color = $record->category->color ?? '#000000';
93
                         $color = $record->category->color ?? '#000000';
91
 
94
 
92
                         return "<span style='display: inline-block; width: 8px; height: 8px; background-color: {$color}; border-radius: 50%; margin-right: 3px;'></span> {$state}";
95
                         return "<span style='display: inline-block; width: 8px; height: 8px; background-color: {$color}; border-radius: 50%; margin-right: 3px;'></span> {$state}";
98
                     ->color(static fn (Transaction $record) => $record->type === 'expense' ? 'danger' : null)
101
                     ->color(static fn (Transaction $record) => $record->type === 'expense' ? 'danger' : null)
99
                     ->currency(static fn (Transaction $record) => $record->bankAccount->account->currency_code ?? 'USD', true),
102
                     ->currency(static fn (Transaction $record) => $record->bankAccount->account->currency_code ?? 'USD', true),
100
             ])
103
             ])
104
+            ->defaultSort('posted_at', 'desc')
101
             ->filters([
105
             ->filters([
102
                 //
106
                 //
103
             ])
107
             ])

+ 0
- 38
app/Http/Controllers/PlaidController.php 查看文件

1
-<?php
2
-
3
-namespace App\Http\Controllers;
4
-
5
-use App\Services\PlaidService;
6
-use Illuminate\Http\Request;
7
-use Illuminate\Routing\Controller;
8
-
9
-class PlaidController extends Controller
10
-{
11
-    /**
12
-     * Handle the incoming request.
13
-     */
14
-    public function __invoke(Request $request, PlaidService $plaidService)
15
-    {
16
-        $publicTokenResponse = $plaidService->createSandboxPublicToken();
17
-        $publicToken = $publicTokenResponse->public_token;
18
-
19
-        $accessTokenResponse = $plaidService->exchangePublicToken($publicToken);
20
-        $accessToken = $accessTokenResponse->access_token;
21
-
22
-        $authResponse = $plaidService->getAuth($accessToken);
23
-        $account = $authResponse;
24
-
25
-        $institutionId = $account->item->institution_id;
26
-
27
-        $institutionResponse = $plaidService->getInstitution($institutionId);
28
-
29
-        return response()->json([
30
-            'public_token' => $publicToken,
31
-            'public_token_response' => $publicTokenResponse,
32
-            'access_token_response' => $accessTokenResponse,
33
-            'access_token' => $accessToken,
34
-            'account' => $account,
35
-            'institution' => $institutionResponse,
36
-        ]);
37
-    }
38
-}

+ 2
- 0
app/Listeners/CreateConnectedAccount.php 查看文件

69
             'external_account_id' => $plaidAccount->account_id,
69
             'external_account_id' => $plaidAccount->account_id,
70
             'access_token' => $accessToken,
70
             'access_token' => $accessToken,
71
             'item_id' => $authResponse->item->item_id,
71
             'item_id' => $authResponse->item->item_id,
72
+            'currency_code' => $plaidAccount->balances->iso_currency_code ?? 'USD',
73
+            'current_balance' => $plaidAccount->balances->current ?? 0,
72
             'name' => $plaidAccount->name,
74
             'name' => $plaidAccount->name,
73
             'mask' => $plaidAccount->mask,
75
             'mask' => $plaidAccount->mask,
74
             'type' => $plaidAccount->type,
76
             'type' => $plaidAccount->type,

+ 26
- 249
app/Listeners/HandleTransactionImport.php 查看文件

2
 
2
 
3
 namespace App\Listeners;
3
 namespace App\Listeners;
4
 
4
 
5
-use App\Enums\Accounting\AccountCategory;
6
-use App\Enums\Accounting\AccountType;
7
 use App\Events\StartTransactionImport;
5
 use App\Events\StartTransactionImport;
8
 use App\Models\Accounting\Account;
6
 use App\Models\Accounting\Account;
9
-use App\Models\Accounting\AccountSubtype;
10
-use App\Models\Accounting\Transaction;
11
-use App\Models\Banking\BankAccount;
12
 use App\Models\Banking\ConnectedBankAccount;
7
 use App\Models\Banking\ConnectedBankAccount;
13
 use App\Models\Company;
8
 use App\Models\Company;
14
-use App\Models\Setting\Category;
15
-use App\Models\Setting\Currency;
9
+use App\Services\AccountService;
10
+use App\Services\BankAccountService;
16
 use App\Services\PlaidService;
11
 use App\Services\PlaidService;
17
-use App\Utilities\Currency\CurrencyAccessor;
12
+use App\Services\TransactionService;
18
 use Illuminate\Support\Carbon;
13
 use Illuminate\Support\Carbon;
19
 use Illuminate\Support\Facades\DB;
14
 use Illuminate\Support\Facades\DB;
20
-use Illuminate\Support\Facades\Log;
21
-use Illuminate\Support\Str;
22
 
15
 
23
 class HandleTransactionImport
16
 class HandleTransactionImport
24
 {
17
 {
25
     protected PlaidService $plaid;
18
     protected PlaidService $plaid;
26
 
19
 
20
+    protected BankAccountService $bankAccountService;
21
+
22
+    protected AccountService $accountService;
23
+
24
+    protected TransactionService $transactionService;
25
+
27
     /**
26
     /**
28
      * Create the event listener.
27
      * Create the event listener.
29
      */
28
      */
30
-    public function __construct(PlaidService $plaid)
29
+    public function __construct(PlaidService $plaid, BankAccountService $bankAccountService, AccountService $accountService, TransactionService $transactionService)
31
     {
30
     {
32
         $this->plaid = $plaid;
31
         $this->plaid = $plaid;
32
+        $this->bankAccountService = $bankAccountService;
33
+        $this->accountService = $accountService;
34
+        $this->transactionService = $transactionService;
33
     }
35
     }
34
 
36
 
35
     /**
37
     /**
51
 
53
 
52
         $accessToken = $connectedBankAccount->access_token;
54
         $accessToken = $connectedBankAccount->access_token;
53
 
55
 
54
-        $bankAccount = $selectedBankAccountId === 'new'
55
-            ? $this->processNewBankAccount($company, $connectedBankAccount, $accessToken)
56
-            : BankAccount::find($selectedBankAccountId);
56
+        $bankAccount = $this->bankAccountService->getOrProcessBankAccount($company, $connectedBankAccount, $selectedBankAccountId);
57
+        $account = $this->accountService->getOrProcessAccount($bankAccount, $company, $connectedBankAccount);
57
 
58
 
58
-        if ($bankAccount) {
59
-            $connectedBankAccount->update([
60
-                'bank_account_id' => $bankAccount->id,
61
-                'import_transactions' => true,
62
-            ]);
63
-
64
-            $account = $bankAccount->account;
59
+        $connectedBankAccount->update([
60
+            'bank_account_id' => $bankAccount->id,
61
+            'import_transactions' => true,
62
+        ]);
65
 
63
 
66
-            $this->processTransactions($startDate, $company, $connectedBankAccount, $accessToken, $account);
67
-        }
64
+        $this->processTransactions($startDate, $company, $connectedBankAccount, $accessToken, $account);
68
     }
65
     }
69
 
66
 
70
-    public function processTransactions($startDate, Company $company, ConnectedBankAccount $connectedBankAccount, $accessToken, Account $account): void
67
+    public function processTransactions(string $startDate, Company $company, ConnectedBankAccount $connectedBankAccount, string $accessToken, Account $account): void
71
     {
68
     {
72
         $endDate = Carbon::now()->toDateString();
69
         $endDate = Carbon::now()->toDateString();
73
         $startDate = Carbon::parse($startDate)->toDateString();
70
         $startDate = Carbon::parse($startDate)->toDateString();
76
             'account_ids' => [$connectedBankAccount->external_account_id],
73
             'account_ids' => [$connectedBankAccount->external_account_id],
77
         ]);
74
         ]);
78
 
75
 
79
-        if (! empty($transactionsResponse->transactions)) {
80
-            foreach ($transactionsResponse->transactions as $transaction) {
81
-                $this->storeTransaction($company, $account, $connectedBankAccount, $transaction);
82
-            }
83
-        }
84
-    }
76
+        if (filled($transactionsResponse->transactions)) {
77
+            $transactions = array_reverse($transactionsResponse->transactions);
78
+            $currentBalance = $transactionsResponse->accounts[0]->balances->current;
85
 
79
 
86
-    public function storeTransaction(Company $company, Account $account, ConnectedBankAccount $connectedBankAccount, $transaction): void
87
-    {
88
-        if ($account->category === AccountCategory::Asset) {
89
-            $transactionType = $transaction->amount < 0 ? 'income' : 'expense';
90
-        } else {
91
-            $transactionType = $transaction->amount < 0 ? 'expense' : 'income';
80
+            $this->transactionService->createStartingBalanceIfNeeded($company, $account, $connectedBankAccount, $transactions, $currentBalance, $startDate);
81
+            $this->transactionService->storeTransactions($company, $account, $connectedBankAccount, $transactions);
92
         }
82
         }
93
-
94
-        $method = $transactionType === 'income' ? 'deposit' : 'withdrawal';
95
-        $paymentChannel = $transaction->payment_channel ?? 'other';
96
-        $category = $this->getCategoryFromTransaction($company, $transaction, $transactionType);
97
-        $chartAccount = $category->account ?? $this->getChartFromTransaction($company, $transaction, $transactionType);
98
-
99
-        $postedAt = $transaction->datetime ?? Carbon::parse($transaction->date)->toDateTimeString();
100
-
101
-        $description = $transaction->original_description ?? $transaction->name;
102
-
103
-        Log::info('Transaction description:', [
104
-            'name' => $transaction->name,
105
-            'description' => $description,
106
-            'amount' => $transaction->amount,
107
-            'detailedCategory' => $transaction->personal_finance_category->detailed,
108
-            'primaryCategory' => $transaction->personal_finance_category->primary,
109
-        ]);
110
-
111
-        $transactionRecord = $account->transactions()->create([
112
-            'company_id' => $company->id,
113
-            'category_id' => $category->id,
114
-            'bank_account_id' => $connectedBankAccount->bank_account_id,
115
-            'type' => $transactionType,
116
-            'amount' => abs($transaction->amount),
117
-            'method' => $method,
118
-            'payment_channel' => $paymentChannel,
119
-            'posted_at' => $postedAt,
120
-            'description' => $description,
121
-            'pending' => $transaction->pending,
122
-            'reviewed' => false,
123
-        ]);
124
-
125
-        $this->createJournalEntries($company, $account, $transactionRecord, $chartAccount);
126
-    }
127
-
128
-    public function createJournalEntries(Company $company, Account $account, Transaction $transaction, Account $chartAccount): void
129
-    {
130
-        // For an expense (withdrawal) transaction, we need to credit the liability or asset account ($account), and debit the expense account ($chartAccount)
131
-        // For an income (deposit) transaction, we need to debit the liability or asset account ($account), and credit the revenue account ($chartAccount)
132
-        // Debiting an Asset account increases its balance. Crediting an Asset account decreases its balance.
133
-        // Crediting a Liability account increases its balance. Debiting a Liability account decreases its balance.
134
-        // Expense accounts should always be debited. Revenue accounts should always be credited.
135
-        $debitAccount = $transaction->type === 'expense' ? $chartAccount : $account;
136
-        $creditAccount = $transaction->type === 'expense' ? $account : $chartAccount;
137
-
138
-        $amount = $transaction->amount;
139
-
140
-        $debitAccount->journalEntries()->create([
141
-            'company_id' => $company->id,
142
-            'transaction_id' => $transaction->id,
143
-            'type' => 'debit',
144
-            'amount' => $amount,
145
-            'description' => $transaction->description,
146
-        ]);
147
-
148
-        $creditAccount->journalEntries()->create([
149
-            'company_id' => $company->id,
150
-            'transaction_id' => $transaction->id,
151
-            'type' => 'credit',
152
-            'amount' => $amount,
153
-            'description' => $transaction->description,
154
-        ]);
155
-    }
156
-
157
-    public function getCategoryFromTransaction(Company $company, $transaction, string $transactionType): Category
158
-    {
159
-        $acceptableConfidenceLevels = ['VERY_HIGH', 'HIGH'];
160
-
161
-        $userCategories = $company->categories()->get();
162
-        $plaidDetail = $transaction->personal_finance_category->detailed ?? null;
163
-        $plaidPrimary = $transaction->personal_finance_category->primary ?? null;
164
-
165
-        $category = null;
166
-
167
-        if ($plaidDetail !== null && in_array($transaction->personal_finance_category->confidence_level, $acceptableConfidenceLevels, true)) {
168
-            $category = $this->matchCategory($userCategories, $plaidDetail, $transactionType);
169
-        }
170
-
171
-        if ($plaidPrimary !== null && ($category === null || $this->isUncategorized($category))) {
172
-            $category = $this->matchCategory($userCategories, $plaidPrimary, $transactionType);
173
-        }
174
-
175
-        return $category ?? $this->getUncategorizedCategory($company, $transaction, $transactionType);
176
-    }
177
-
178
-    public function isUncategorized(Category $category): bool
179
-    {
180
-        return Str::contains(strtolower($category->name), 'other');
181
-    }
182
-
183
-    public function matchCategory($userCategories, $plaidCategory, string $transactionType): ?Category
184
-    {
185
-        $plaidWords = explode(' ', strtolower($plaidCategory));
186
-
187
-        $bestMatchCategory = null;
188
-        $bestMatchScore = 0; // Higher is better
189
-
190
-        foreach ($userCategories as $category) {
191
-            if (strtolower($category->type->value) !== strtolower($transactionType)) {
192
-                continue;
193
-            }
194
-
195
-            $categoryWords = explode(' ', strtolower($category->name));
196
-            $matchScore = count(array_intersect($plaidWords, $categoryWords));
197
-
198
-            if ($matchScore > $bestMatchScore) {
199
-                $bestMatchScore = $matchScore;
200
-                $bestMatchCategory = $category;
201
-            }
202
-        }
203
-
204
-        return $bestMatchCategory;
205
-    }
206
-
207
-    public function getUncategorizedCategory(Company $company, $transaction, string $transactionType): Category
208
-    {
209
-        $uncategorizedCategoryName = 'Other ' . ucfirst($transactionType);
210
-        $uncategorizedCategory = $company->categories()->where('type', $transactionType)->where('name', $uncategorizedCategoryName)->first();
211
-
212
-        if ($uncategorizedCategory === null) {
213
-            $uncategorizedCategory = $company->categories()->where('type', $transactionType)->where('name', 'Other')->first();
214
-
215
-            if ($uncategorizedCategory === null) {
216
-                $uncategorizedCategory = $company->categories()->where('name', 'Other')->first();
217
-            }
218
-        }
219
-
220
-        return $uncategorizedCategory;
221
-    }
222
-
223
-    public function getChartFromTransaction(Company $company, $transaction, string $transactionType): Account
224
-    {
225
-        if ($transactionType === 'income') {
226
-            $chart = $company->accounts()->where('type', AccountType::UncategorizedRevenue)->where('name', 'Uncategorized Income')->first();
227
-        } else {
228
-            $chart = $company->accounts()->where('type', AccountType::UncategorizedExpense)->where('name', 'Uncategorized Expense')->first();
229
-        }
230
-
231
-        return $chart;
232
-    }
233
-
234
-    public function processNewBankAccount(Company $company, ConnectedBankAccount $connectedBankAccount, $accessToken): BankAccount
235
-    {
236
-        $bankAccount = $connectedBankAccount->bankAccount()->create([
237
-            'company_id' => $company->id,
238
-            'institution_id' => $connectedBankAccount->institution_id,
239
-            'type' => $connectedBankAccount->type,
240
-            'number' => $connectedBankAccount->mask,
241
-            'enabled' => BankAccount::where('company_id', $company->id)->where('enabled', true)->doesntExist(),
242
-        ]);
243
-
244
-        $this->mapAccountDetails($bankAccount, $company, $accessToken, $connectedBankAccount);
245
-
246
-        return $bankAccount;
247
-    }
248
-
249
-    public function mapAccountDetails(BankAccount $bankAccount, Company $company, $accessToken, ConnectedBankAccount $connectedBankAccount): void
250
-    {
251
-        $this->ensureCurrencyExists($company->id, 'USD');
252
-
253
-        $accountSubtype = $this->getAccountSubtype($bankAccount->type->value);
254
-
255
-        $accountSubtypeId = $this->resolveAccountSubtypeId($company, $accountSubtype);
256
-
257
-        $bankAccount->account()->create([
258
-            'company_id' => $company->id,
259
-            'name' => $connectedBankAccount->name,
260
-            'currency_code' => 'USD',
261
-            'description' => $connectedBankAccount->name,
262
-            'subtype_id' => $accountSubtypeId,
263
-            'active' => true,
264
-        ]);
265
-    }
266
-
267
-    public function ensureCurrencyExists(int $companyId, string $currencyCode): void
268
-    {
269
-        $defaultCurrency = CurrencyAccessor::getDefaultCurrency();
270
-
271
-        $hasDefaultCurrency = $defaultCurrency !== null;
272
-
273
-        $currency_code = currency($currencyCode);
274
-
275
-        Currency::firstOrCreate([
276
-            'company_id' => $companyId,
277
-            'code' => $currencyCode,
278
-        ], [
279
-            'name' => $currency_code->getName(),
280
-            'rate' => $currency_code->getRate(),
281
-            'precision' => $currency_code->getPrecision(),
282
-            'symbol' => $currency_code->getSymbol(),
283
-            'symbol_first' => $currency_code->isSymbolFirst(),
284
-            'decimal_mark' => $currency_code->getDecimalMark(),
285
-            'thousands_separator' => $currency_code->getThousandsSeparator(),
286
-            'enabled' => ! $hasDefaultCurrency,
287
-        ]);
288
-    }
289
-
290
-    public function getAccountSubtype(string $plaidType): string
291
-    {
292
-        return match ($plaidType) {
293
-            'depository' => 'Cash and Cash Equivalents',
294
-            'credit' => 'Short-Term Borrowings',
295
-            'loan' => 'Long-Term Borrowings',
296
-            'investment' => 'Long-Term Investments',
297
-            'other' => 'Other Current Assets',
298
-        };
299
-    }
300
-
301
-    public function resolveAccountSubtypeId(Company $company, string $accountSubtype): int
302
-    {
303
-        return AccountSubtype::where('company_id', $company->id)
304
-            ->where('name', $accountSubtype)
305
-            ->value('id');
306
     }
83
     }
307
 }
84
 }

+ 0
- 177
app/Listeners/PopulateAccountFromPlaid.php 查看文件

1
-<?php
2
-
3
-namespace App\Listeners;
4
-
5
-use App\Events\PlaidSuccess;
6
-use App\Models\Accounting\AccountSubtype;
7
-use App\Models\Banking\BankAccount;
8
-use App\Models\Banking\Institution;
9
-use App\Models\Company;
10
-use App\Models\Setting\Currency;
11
-use App\Services\PlaidService;
12
-use App\Utilities\Currency\CurrencyAccessor;
13
-use Illuminate\Support\Facades\DB;
14
-
15
-class PopulateAccountFromPlaid
16
-{
17
-    protected PlaidService $plaid;
18
-
19
-    /**
20
-     * Create the event listener.
21
-     */
22
-    public function __construct(PlaidService $plaid)
23
-    {
24
-        $this->plaid = $plaid;
25
-    }
26
-
27
-    /**
28
-     * Handle the event.
29
-     */
30
-    public function handle(PlaidSuccess $event): void
31
-    {
32
-        DB::transaction(function () use ($event) {
33
-            $this->processPlaidSuccess($event);
34
-        });
35
-    }
36
-
37
-    public function processPlaidSuccess(PlaidSuccess $event): void
38
-    {
39
-        $accessToken = $event->accessToken;
40
-
41
-        $company = $event->company;
42
-
43
-        $authResponse = $this->plaid->getAccounts($accessToken);
44
-
45
-        $institutionResponse = $this->plaid->getInstitution($authResponse->item->institution_id, $company->profile->country);
46
-
47
-        $this->processInstitution($authResponse, $institutionResponse, $company);
48
-    }
49
-
50
-    public function processInstitution($authResponse, $institutionResponse, Company $company): void
51
-    {
52
-        $institution = Institution::updateOrCreate([
53
-            'external_institution_id' => $authResponse->item->institution_id ?? null,
54
-        ], [
55
-            'name' => $institutionResponse->institution->name ?? null,
56
-            'logo' => $institutionResponse->institution->logo ?? null,
57
-            'website' => $institutionResponse->institution->url ?? null,
58
-        ]);
59
-
60
-        foreach ($authResponse->accounts as $plaidAccount) {
61
-            $this->processBankAccount($plaidAccount, $company, $institution, $authResponse);
62
-        }
63
-    }
64
-
65
-    public function processBankAccount($plaidAccount, Company $company, Institution $institution, $authResponse): void
66
-    {
67
-        $identifierHash = md5($institution->external_institution_id . $plaidAccount->name . $plaidAccount->mask);
68
-
69
-        $bankAccount = BankAccount::updateOrCreate([
70
-            'company_id' => $company->id,
71
-            'identifier' => $identifierHash,
72
-        ], [
73
-            'is_connected_account' => true,
74
-            'external_account_id' => $plaidAccount->account_id,
75
-            'item_id' => $authResponse->item->item_id,
76
-            'enabled' => BankAccount::where('company_id', $company->id)->where('enabled', true)->doesntExist(),
77
-            'type' => $plaidAccount->type,
78
-            'number' => $plaidAccount->mask,
79
-            'institution_id' => $institution->id,
80
-        ]);
81
-
82
-        $this->mapAccountDetails($bankAccount, $plaidAccount, $company);
83
-    }
84
-
85
-    public function mapAccountDetails(BankAccount $bankAccount, $plaidAccount, Company $company): void
86
-    {
87
-        $this->ensureCurrencyExists($company->id, $plaidAccount->balances->iso_currency_code);
88
-
89
-        $accountSubtype = $this->getAccountSubtype($plaidAccount->type);
90
-
91
-        $accountSubtypeId = $this->resolveAccountSubtypeId($company, $accountSubtype);
92
-
93
-        $bankAccount->account()->updateOrCreate([], [
94
-            'name' => $plaidAccount->name,
95
-            'currency_code' => $plaidAccount->balances->iso_currency_code,
96
-            'description' => $plaidAccount->official_name ?? $plaidAccount->name,
97
-            'subtype_id' => $accountSubtypeId,
98
-            'active' => true,
99
-        ]);
100
-    }
101
-
102
-    public function getAccountSubtype(string $plaidType): string
103
-    {
104
-        return match ($plaidType) {
105
-            'depository' => 'Cash and Cash Equivalents',
106
-            'credit' => 'Short-Term Borrowings',
107
-            'loan' => 'Long-Term Borrowings',
108
-            'investment' => 'Long-Term Investments',
109
-            'other' => 'Other Current Assets',
110
-        };
111
-    }
112
-
113
-    public function resolveAccountSubtypeId(Company $company, string $accountSubtype): int
114
-    {
115
-        return AccountSubtype::where('company_id', $company->id)
116
-            ->where('name', $accountSubtype)
117
-            ->value('id');
118
-    }
119
-
120
-    public function ensureCurrencyExists(int $companyId, string $currencyCode): void
121
-    {
122
-        $defaultCurrency = CurrencyAccessor::getDefaultCurrency();
123
-
124
-        $hasDefaultCurrency = $defaultCurrency !== null;
125
-
126
-        $currency_code = currency($currencyCode);
127
-
128
-        Currency::firstOrCreate([
129
-            'company_id' => $companyId,
130
-            'code' => $currencyCode,
131
-        ], [
132
-            'name' => $currency_code->getName(),
133
-            'rate' => $currency_code->getRate(),
134
-            'precision' => $currency_code->getPrecision(),
135
-            'symbol' => $currency_code->getSymbol(),
136
-            'symbol_first' => $currency_code->isSymbolFirst(),
137
-            'decimal_mark' => $currency_code->getDecimalMark(),
138
-            'thousands_separator' => $currency_code->getThousandsSeparator(),
139
-            'enabled' => ! $hasDefaultCurrency,
140
-        ]);
141
-    }
142
-
143
-    public function getRoutingNumber($accountId, $numbers): array
144
-    {
145
-        foreach ($numbers as $type => $numberList) {
146
-            foreach ($numberList as $number) {
147
-                if ($number->account_id === $accountId) {
148
-                    return match ($type) {
149
-                        'ach' => ['routing_number' => $number->routing],
150
-                        'bacs' => ['routing_number' => $number->sort_code],
151
-                        'eft' => ['routing_number' => $number->branch],
152
-                        'international' => [
153
-                            'bic' => $number->bic,
154
-                            'iban' => $number->iban,
155
-                        ],
156
-                        default => [],
157
-                    };
158
-                }
159
-            }
160
-        }
161
-
162
-        return [];
163
-    }
164
-
165
-    public function getFullAccountNumber($accountId, $numbers)
166
-    {
167
-        foreach ($numbers as $numberList) {
168
-            foreach ($numberList as $number) {
169
-                if ($number->account_id === $accountId && property_exists($number, 'account')) {
170
-                    return $number->account;
171
-                }
172
-            }
173
-        }
174
-
175
-        return null;
176
-    }
177
-}

+ 0
- 154
app/Listeners/SyncTransactionsFromPlaid.php 查看文件

1
-<?php
2
-
3
-namespace App\Listeners;
4
-
5
-use App\Enums\Accounting\AccountType;
6
-use App\Events\PlaidSuccess;
7
-use App\Models\Accounting\Account;
8
-use App\Models\Company;
9
-use App\Models\Setting\Category;
10
-use App\Services\PlaidService;
11
-use Illuminate\Support\Carbon;
12
-use Illuminate\Support\Str;
13
-
14
-class SyncTransactionsFromPlaid
15
-{
16
-    protected PlaidService $plaid;
17
-
18
-    /**
19
-     * Create the event listener.
20
-     */
21
-    public function __construct(PlaidService $plaid)
22
-    {
23
-        $this->plaid = $plaid;
24
-    }
25
-
26
-    /**
27
-     * Handle the event.
28
-     */
29
-    public function handle(PlaidSuccess $event): void
30
-    {
31
-        $accessToken = $event->accessToken;
32
-        $company = $event->company;
33
-
34
-        $syncResponse = $this->plaid->syncTransactions($accessToken);
35
-
36
-        foreach ($syncResponse->added as $transaction) {
37
-            $this->storeTransaction($company, $transaction);
38
-        }
39
-    }
40
-
41
-    public function storeTransaction(Company $company, $transaction): void
42
-    {
43
-        $account = $company->accounts()->where('external_account_id', $transaction->account_id)->first();
44
-
45
-        if ($account === null) {
46
-            return;
47
-        }
48
-
49
-        $transactionType = $transaction->amount < 0 ? 'income' : 'expense';
50
-        $method = $transactionType === 'income' ? 'deposit' : 'withdrawal';
51
-        $paymentChannel = $transaction->payment_channel ?? 'other';
52
-        $category = $this->getCategoryFromTransaction($company, $transaction, $transactionType);
53
-        $chart = $category->account ?? $this->getChartFromTransaction($company, $transaction, $transactionType);
54
-
55
-        // Use datetime and if null, then use date and convert to datetime
56
-        $postedAt = $transaction->datetime ?? Carbon::parse($transaction->date)->toDateTimeString();
57
-
58
-        $description = $transaction->original_description ?? $transaction->name;
59
-        $cleanDescription = preg_replace('/\\*\\/\\/$/', '', $description);
60
-        $cleanDescription = trim(preg_replace('/\\s+/', ' ', $cleanDescription));
61
-
62
-        $account->transactions()->create([
63
-            'company_id' => $company->id,
64
-            'account_id' => $account->id,
65
-            'category_id' => $category->id,
66
-            'chart_id' => $chart->id,
67
-            'amount' => abs($transaction->amount),
68
-            'type' => $transactionType,
69
-            'method' => $method,
70
-            'payment_channel' => $paymentChannel,
71
-            'posted_at' => $postedAt,
72
-            'description' => $cleanDescription,
73
-            'pending' => $transaction->pending,
74
-            'reviewed' => false,
75
-        ]);
76
-    }
77
-
78
-    public function getCategoryFromTransaction(Company $company, $transaction, string $transactionType): Category
79
-    {
80
-        $acceptableConfidenceLevels = ['VERY_HIGH', 'HIGH'];
81
-
82
-        $userCategories = $company->categories()->get();
83
-        $plaidDetail = $transaction->personal_finance_category->detailed ?? null;
84
-        $plaidPrimary = $transaction->personal_finance_category->primary ?? null;
85
-
86
-        $category = null;
87
-
88
-        if ($plaidDetail !== null && in_array($transaction->personal_finance_category->confidence_level, $acceptableConfidenceLevels, true)) {
89
-            $category = $this->matchCategory($userCategories, $plaidDetail, $transactionType);
90
-        }
91
-
92
-        if ($plaidPrimary !== null && ($category === null || $this->isUncategorized($category))) {
93
-            $category = $this->matchCategory($userCategories, $plaidPrimary, $transactionType);
94
-        }
95
-
96
-        return $category ?? $this->getUncategorizedCategory($company, $transaction, $transactionType);
97
-    }
98
-
99
-    public function isUncategorized(Category $category): bool
100
-    {
101
-        return Str::contains(strtolower($category->name), 'other');
102
-    }
103
-
104
-    public function matchCategory($userCategories, $plaidCategory, string $transactionType): ?Category
105
-    {
106
-        $plaidWords = explode(' ', strtolower($plaidCategory));
107
-
108
-        $bestMatchCategory = null;
109
-        $bestMatchScore = 0; // Higher is better
110
-
111
-        foreach ($userCategories as $category) {
112
-            if (strtolower($category->type->value) !== strtolower($transactionType)) {
113
-                continue;
114
-            }
115
-
116
-            $categoryWords = explode(' ', strtolower($category->name));
117
-            $matchScore = count(array_intersect($plaidWords, $categoryWords));
118
-
119
-            if ($matchScore > $bestMatchScore) {
120
-                $bestMatchScore = $matchScore;
121
-                $bestMatchCategory = $category;
122
-            }
123
-        }
124
-
125
-        return $bestMatchCategory;
126
-    }
127
-
128
-    public function getUncategorizedCategory(Company $company, $transaction, string $transactionType): Category
129
-    {
130
-        $uncategorizedCategoryName = 'Other ' . ucfirst($transactionType);
131
-        $uncategorizedCategory = $company->categories()->where('type', $transactionType)->where('name', $uncategorizedCategoryName)->first();
132
-
133
-        if ($uncategorizedCategory === null) {
134
-            $uncategorizedCategory = $company->categories()->where('type', $transactionType)->where('name', 'Other')->first();
135
-
136
-            if ($uncategorizedCategory === null) {
137
-                $uncategorizedCategory = $company->categories()->where('name', 'Other')->first();
138
-            }
139
-        }
140
-
141
-        return $uncategorizedCategory;
142
-    }
143
-
144
-    public function getChartFromTransaction(Company $company, $transaction, string $transactionType): Account
145
-    {
146
-        if ($transactionType === 'income') {
147
-            $chart = $company->accounts()->where('type', AccountType::OperatingRevenue)->where('name', 'Uncategorized Income')->first();
148
-        } else {
149
-            $chart = $company->accounts()->where('type', AccountType::OperatingExpense)->where('name', 'Uncategorized Expense')->first();
150
-        }
151
-
152
-        return $chart;
153
-    }
154
-}

+ 4
- 0
app/Models/Accounting/Account.php 查看文件

7
 use App\Enums\Accounting\AccountType;
7
 use App\Enums\Accounting\AccountType;
8
 use App\Models\Setting\Category;
8
 use App\Models\Setting\Category;
9
 use App\Models\Setting\Currency;
9
 use App\Models\Setting\Currency;
10
+use App\Observers\AccountObserver;
10
 use App\Traits\Blamable;
11
 use App\Traits\Blamable;
11
 use App\Traits\CompanyOwned;
12
 use App\Traits\CompanyOwned;
12
 use Database\Factories\Accounting\AccountFactory;
13
 use Database\Factories\Accounting\AccountFactory;
14
+use Illuminate\Database\Eloquent\Attributes\ObservedBy;
13
 use Illuminate\Database\Eloquent\Factories\Factory;
15
 use Illuminate\Database\Eloquent\Factories\Factory;
14
 use Illuminate\Database\Eloquent\Factories\HasFactory;
16
 use Illuminate\Database\Eloquent\Factories\HasFactory;
15
 use Illuminate\Database\Eloquent\Model;
17
 use Illuminate\Database\Eloquent\Model;
19
 use Illuminate\Database\Eloquent\Relations\MorphTo;
21
 use Illuminate\Database\Eloquent\Relations\MorphTo;
20
 use Wallo\FilamentCompanies\FilamentCompanies;
22
 use Wallo\FilamentCompanies\FilamentCompanies;
21
 
23
 
24
+#[ObservedBy(AccountObserver::class)]
22
 class Account extends Model
25
 class Account extends Model
23
 {
26
 {
24
     use Blamable;
27
     use Blamable;
56
         'starting_balance' => MoneyCast::class,
59
         'starting_balance' => MoneyCast::class,
57
         'debit_balance' => MoneyCast::class,
60
         'debit_balance' => MoneyCast::class,
58
         'credit_balance' => MoneyCast::class,
61
         'credit_balance' => MoneyCast::class,
62
+        'net_movement' => MoneyCast::class,
59
         'ending_balance' => MoneyCast::class,
63
         'ending_balance' => MoneyCast::class,
60
         'active' => 'boolean',
64
         'active' => 'boolean',
61
         'default' => 'boolean',
65
         'default' => 'boolean',

+ 3
- 0
app/Models/Accounting/JournalEntry.php 查看文件

4
 
4
 
5
 use App\Casts\MoneyCast;
5
 use App\Casts\MoneyCast;
6
 use App\Models\Banking\BankAccount;
6
 use App\Models\Banking\BankAccount;
7
+use App\Observers\JournalEntryObserver;
7
 use App\Traits\Blamable;
8
 use App\Traits\Blamable;
8
 use App\Traits\CompanyOwned;
9
 use App\Traits\CompanyOwned;
9
 use Database\Factories\Accounting\JournalEntryFactory;
10
 use Database\Factories\Accounting\JournalEntryFactory;
11
+use Illuminate\Database\Eloquent\Attributes\ObservedBy;
10
 use Illuminate\Database\Eloquent\Factories\Factory;
12
 use Illuminate\Database\Eloquent\Factories\Factory;
11
 use Illuminate\Database\Eloquent\Factories\HasFactory;
13
 use Illuminate\Database\Eloquent\Factories\HasFactory;
12
 use Illuminate\Database\Eloquent\Model;
14
 use Illuminate\Database\Eloquent\Model;
13
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
15
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
14
 use Wallo\FilamentCompanies\FilamentCompanies;
16
 use Wallo\FilamentCompanies\FilamentCompanies;
15
 
17
 
18
+#[ObservedBy(JournalEntryObserver::class)]
16
 class JournalEntry extends Model
19
 class JournalEntry extends Model
17
 {
20
 {
18
     use Blamable;
21
     use Blamable;

+ 3
- 0
app/Models/Accounting/Transaction.php 查看文件

6
 use App\Models\Banking\BankAccount;
6
 use App\Models\Banking\BankAccount;
7
 use App\Models\Common\Contact;
7
 use App\Models\Common\Contact;
8
 use App\Models\Setting\Category;
8
 use App\Models\Setting\Category;
9
+use App\Observers\TransactionObserver;
9
 use App\Traits\Blamable;
10
 use App\Traits\Blamable;
10
 use App\Traits\CompanyOwned;
11
 use App\Traits\CompanyOwned;
11
 use Database\Factories\Accounting\TransactionFactory;
12
 use Database\Factories\Accounting\TransactionFactory;
13
+use Illuminate\Database\Eloquent\Attributes\ObservedBy;
12
 use Illuminate\Database\Eloquent\Factories\Factory;
14
 use Illuminate\Database\Eloquent\Factories\Factory;
13
 use Illuminate\Database\Eloquent\Factories\HasFactory;
15
 use Illuminate\Database\Eloquent\Factories\HasFactory;
14
 use Illuminate\Database\Eloquent\Model;
16
 use Illuminate\Database\Eloquent\Model;
16
 use Illuminate\Database\Eloquent\Relations\HasMany;
18
 use Illuminate\Database\Eloquent\Relations\HasMany;
17
 use Wallo\FilamentCompanies\FilamentCompanies;
19
 use Wallo\FilamentCompanies\FilamentCompanies;
18
 
20
 
21
+#[ObservedBy(TransactionObserver::class)]
19
 class Transaction extends Model
22
 class Transaction extends Model
20
 {
23
 {
21
     use Blamable;
24
     use Blamable;

+ 3
- 0
app/Models/Banking/BankAccount.php 查看文件

4
 
4
 
5
 use App\Enums\BankAccountType;
5
 use App\Enums\BankAccountType;
6
 use App\Models\Accounting\Account;
6
 use App\Models\Accounting\Account;
7
+use App\Observers\BankAccountObserver;
7
 use App\Traits\Blamable;
8
 use App\Traits\Blamable;
8
 use App\Traits\CompanyOwned;
9
 use App\Traits\CompanyOwned;
9
 use App\Traits\HasDefault;
10
 use App\Traits\HasDefault;
10
 use App\Traits\SyncsWithCompanyDefaults;
11
 use App\Traits\SyncsWithCompanyDefaults;
11
 use Database\Factories\Banking\BankAccountFactory;
12
 use Database\Factories\Banking\BankAccountFactory;
13
+use Illuminate\Database\Eloquent\Attributes\ObservedBy;
12
 use Illuminate\Database\Eloquent\Casts\Attribute;
14
 use Illuminate\Database\Eloquent\Casts\Attribute;
13
 use Illuminate\Database\Eloquent\Factories\Factory;
15
 use Illuminate\Database\Eloquent\Factories\Factory;
14
 use Illuminate\Database\Eloquent\Factories\HasFactory;
16
 use Illuminate\Database\Eloquent\Factories\HasFactory;
18
 use Illuminate\Database\Eloquent\Relations\MorphOne;
20
 use Illuminate\Database\Eloquent\Relations\MorphOne;
19
 use Wallo\FilamentCompanies\FilamentCompanies;
21
 use Wallo\FilamentCompanies\FilamentCompanies;
20
 
22
 
23
+#[ObservedBy(BankAccountObserver::class)]
21
 class BankAccount extends Model
24
 class BankAccount extends Model
22
 {
25
 {
23
     use Blamable;
26
     use Blamable;

+ 2
- 0
app/Models/Banking/ConnectedBankAccount.php 查看文件

25
         'access_token',
25
         'access_token',
26
         'identifier',
26
         'identifier',
27
         'item_id',
27
         'item_id',
28
+        'currency_code',
29
+        'current_balance',
28
         'name',
30
         'name',
29
         'mask',
31
         'mask',
30
         'type',
32
         'type',

+ 45
- 42
app/Observers/JournalEntryObserver.php 查看文件

15
     {
15
     {
16
         $account = $journalEntry->account;
16
         $account = $journalEntry->account;
17
 
17
 
18
+        $amount = $this->cleanAmount($journalEntry->amount);
19
+
18
         if ($account) {
20
         if ($account) {
19
-            $this->adjustBalance($account, $journalEntry->type, $journalEntry->amount);
21
+            $this->adjustBalance($account, $journalEntry->type, $amount);
20
         }
22
         }
21
     }
23
     }
22
 
24
 
23
-    private function updateEndingBalance(Account $account): void
25
+    private function cleanAmount($amount): string
24
     {
26
     {
25
-        $netMovementStrategy = match ($account->category) {
26
-            AccountCategory::Asset, AccountCategory::Expense => $account->debit_balance - $account->credit_balance,
27
-            AccountCategory::Liability, AccountCategory::Equity, AccountCategory::Revenue => $account->credit_balance - $account->debit_balance,
28
-        };
27
+        return str_replace(',', '', $amount);
28
+    }
29
 
29
 
30
-        $account->net_movement = $netMovementStrategy;
30
+    private function adjustBalance(Account $account, $type, $amount): void
31
+    {
32
+        $debitBalance = $this->cleanAmount($account->debit_balance);
33
+        $creditBalance = $this->cleanAmount($account->credit_balance);
31
 
34
 
32
-        if (in_array($account->category, [AccountCategory::Asset, AccountCategory::Liability, AccountCategory::Equity], true)) {
33
-            $account->ending_balance = $account->starting_balance + $account->net_movement;
35
+        if ($type === 'debit') {
36
+            $account->debit_balance = bcadd($debitBalance, $amount, 2);
37
+        } elseif ($type === 'credit') {
38
+            $account->credit_balance = bcadd($creditBalance, $amount, 2);
34
         }
39
         }
35
 
40
 
36
-        $account->save();
41
+        $this->updateNetMovement($account);
42
+        $this->updateEndingBalance($account);
37
     }
43
     }
38
 
44
 
39
-    /**
40
-     * Handle the JournalEntry "updated" event.
41
-     */
42
-    public function updated(JournalEntry $journalEntry): void
45
+    private function updateNetMovement(Account $account): void
43
     {
46
     {
44
-        $accountChanged = $journalEntry->wasChanged('account_id');
45
-        $amountChanged = $journalEntry->wasChanged('amount');
46
-        $typeChanged = $journalEntry->wasChanged('type');
47
-
48
-        $originalAccountId = $journalEntry->getOriginal('account_id');
49
-        $originalAmount = $journalEntry->getOriginal('amount');
50
-        $originalType = $journalEntry->getOriginal('type');
47
+        $debitBalance = $this->cleanAmount($account->debit_balance);
48
+        $creditBalance = $this->cleanAmount($account->credit_balance);
51
 
49
 
52
-        if ($accountChanged || $amountChanged || $typeChanged) {
53
-            // Revert the effects of the original journal entry
54
-            $originalAccount = Account::find($originalAccountId);
55
-
56
-            if ($originalAccount) {
57
-                $this->adjustBalance($originalAccount, $originalType, -$originalAmount);
58
-            }
59
-        }
50
+        $netMovementStrategy = match ($account->category) {
51
+            AccountCategory::Asset, AccountCategory::Expense => bcsub($debitBalance, $creditBalance, 2),
52
+            AccountCategory::Liability, AccountCategory::Equity, AccountCategory::Revenue => bcsub($creditBalance, $debitBalance, 2),
53
+        };
60
 
54
 
61
-        $newAccount = ($accountChanged) ? Account::find($journalEntry->account_id) : $journalEntry->account;
55
+        $account->net_movement = $netMovementStrategy;
62
 
56
 
63
-        if ($newAccount) {
64
-            $this->adjustBalance($newAccount, $journalEntry->type, $journalEntry->amount);
65
-        }
57
+        $account->save();
66
     }
58
     }
67
 
59
 
68
-    private function adjustBalance(Account $account, $type, $amount): void
60
+    private function updateEndingBalance(Account $account): void
69
     {
61
     {
70
-        if ($type === 'debit') {
71
-            $account->debit_balance += $amount;
72
-        } elseif ($type === 'credit') {
73
-            $account->credit_balance += $amount;
62
+        $startingBalance = $this->cleanAmount($account->starting_balance);
63
+        $netMovement = $this->cleanAmount($account->net_movement);
64
+
65
+        if (in_array($account->category, [AccountCategory::Asset, AccountCategory::Liability, AccountCategory::Equity], true)) {
66
+            $account->ending_balance = bcadd($startingBalance, $netMovement, 2);
74
         }
67
         }
75
 
68
 
76
-        $this->updateEndingBalance($account);
69
+        $account->save();
77
     }
70
     }
78
 
71
 
79
     /**
72
     /**
80
-     * Handle the JournalEntry "deleted" event.
73
+     * Handle the JournalEntry "deleting" event.
81
      */
74
      */
82
-    public function deleted(JournalEntry $journalEntry): void
75
+    public function deleting(JournalEntry $journalEntry): void
83
     {
76
     {
84
         $account = $journalEntry->account;
77
         $account = $journalEntry->account;
85
 
78
 
86
         if ($account) {
79
         if ($account) {
87
-            $this->adjustBalance($account, $journalEntry->type, -$journalEntry->amount);
80
+            $amount = $this->cleanAmount($journalEntry->amount);
81
+
82
+            $this->adjustBalance($account, $journalEntry->type, -$amount);
88
         }
83
         }
89
     }
84
     }
90
 
85
 
86
+    /**
87
+     * Handle the JournalEntry "deleted" event.
88
+     */
89
+    public function deleted(JournalEntry $journalEntry): void
90
+    {
91
+        //
92
+    }
93
+
91
     /**
94
     /**
92
      * Handle the JournalEntry "restored" event.
95
      * Handle the JournalEntry "restored" event.
93
      */
96
      */

+ 60
- 0
app/Observers/TransactionObserver.php 查看文件

1
+<?php
2
+
3
+namespace App\Observers;
4
+
5
+use App\Models\Accounting\JournalEntry;
6
+use App\Models\Accounting\Transaction;
7
+use Illuminate\Support\Facades\DB;
8
+
9
+class TransactionObserver
10
+{
11
+    /**
12
+     * Handle the Transaction "created" event.
13
+     */
14
+    public function created(Transaction $transaction): void
15
+    {
16
+        //
17
+    }
18
+
19
+    /**
20
+     * Handle the Transaction "updated" event.
21
+     */
22
+    public function updated(Transaction $transaction): void
23
+    {
24
+        //
25
+    }
26
+
27
+    /**
28
+     * Handle the Transaction "deleting" event.
29
+     */
30
+    public function deleting(Transaction $transaction): void
31
+    {
32
+        DB::transaction(static function () use ($transaction) {
33
+            $transaction->journalEntries()->each(fn (JournalEntry $entry) => $entry->delete());
34
+        });
35
+    }
36
+
37
+    /**
38
+     * Handle the Transaction "deleted" event.
39
+     */
40
+    public function deleted(Transaction $transaction): void
41
+    {
42
+        //
43
+    }
44
+
45
+    /**
46
+     * Handle the Transaction "restored" event.
47
+     */
48
+    public function restored(Transaction $transaction): void
49
+    {
50
+        //
51
+    }
52
+
53
+    /**
54
+     * Handle the Transaction "force deleted" event.
55
+     */
56
+    public function forceDeleted(Transaction $transaction): void
57
+    {
58
+        //
59
+    }
60
+}

+ 1
- 15
app/Providers/EventServiceProvider.php 查看文件

16
 use App\Listeners\CreateCompanyDefaults;
16
 use App\Listeners\CreateCompanyDefaults;
17
 use App\Listeners\CreateConnectedAccount;
17
 use App\Listeners\CreateConnectedAccount;
18
 use App\Listeners\HandleTransactionImport;
18
 use App\Listeners\HandleTransactionImport;
19
-use App\Listeners\PopulateAccountFromPlaid;
20
 use App\Listeners\SyncAssociatedModels;
19
 use App\Listeners\SyncAssociatedModels;
21
-use App\Listeners\SyncTransactionsFromPlaid;
22
 use App\Listeners\SyncWithCompanyDefaults;
20
 use App\Listeners\SyncWithCompanyDefaults;
23
 use App\Listeners\UpdateAccountBalances;
21
 use App\Listeners\UpdateAccountBalances;
24
 use App\Listeners\UpdateCurrencyRates;
22
 use App\Listeners\UpdateCurrencyRates;
25
-use App\Models\Accounting\Account;
26
-use App\Models\Accounting\JournalEntry;
27
-use App\Models\Banking\BankAccount;
28
 use App\Models\Setting\Currency;
23
 use App\Models\Setting\Currency;
29
-use App\Observers\AccountObserver;
30
-use App\Observers\BankAccountObserver;
31
 use App\Observers\CurrencyObserver;
24
 use App\Observers\CurrencyObserver;
32
-use App\Observers\JournalEntryObserver;
33
 use Illuminate\Auth\Events\Registered;
25
 use Illuminate\Auth\Events\Registered;
34
 use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
26
 use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
35
 use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
27
 use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
67
         ],
59
         ],
68
         PlaidSuccess::class => [
60
         PlaidSuccess::class => [
69
             CreateConnectedAccount::class,
61
             CreateConnectedAccount::class,
70
-            // PopulateAccountFromPlaid::class,
71
-            // SyncTransactionsFromPlaid::class,
72
         ],
62
         ],
73
         StartTransactionImport::class => [
63
         StartTransactionImport::class => [
74
-            // SyncTransactionsFromPlaid::class,
75
             HandleTransactionImport::class,
64
             HandleTransactionImport::class,
76
         ],
65
         ],
77
     ];
66
     ];
82
      * @var array<string, string|object|array<int, string|object>>
71
      * @var array<string, string|object|array<int, string|object>>
83
      */
72
      */
84
     protected $observers = [
73
     protected $observers = [
85
-        Currency::class => [CurrencyObserver::class],
86
-        BankAccount::class => [BankAccountObserver::class],
87
-        Account::class => [AccountObserver::class],
88
-        JournalEntry::class => [JournalEntryObserver::class],
74
+        // Currency::class => [CurrencyObserver::class],
89
     ];
75
     ];
90
 
76
 
91
     /**
77
     /**

+ 1
- 0
app/Providers/FilamentCompaniesServiceProvider.php 查看文件

90
                 'gray' => Color::Gray,
90
                 'gray' => Color::Gray,
91
             ])
91
             ])
92
             ->viteTheme('resources/css/filament/company/theme.css')
92
             ->viteTheme('resources/css/filament/company/theme.css')
93
+            ->brandLogo(static fn () => view('components.icons.logo'))
93
             ->tenant(Company::class)
94
             ->tenant(Company::class)
94
             ->tenantProfile(CompanySettings::class)
95
             ->tenantProfile(CompanySettings::class)
95
             ->tenantRegistration(CreateCompany::class)
96
             ->tenantRegistration(CreateCompany::class)

+ 90
- 0
app/Services/AccountService.php 查看文件

1
+<?php
2
+
3
+namespace App\Services;
4
+
5
+use App\Models\Accounting\Account;
6
+use App\Models\Banking\BankAccount;
7
+use App\Models\Banking\ConnectedBankAccount;
8
+use App\Models\Company;
9
+use App\Models\Setting\Currency;
10
+use Illuminate\Database\Eloquent\ModelNotFoundException;
11
+
12
+class AccountService
13
+{
14
+    public function getOrProcessAccount(BankAccount $bankAccount, Company $company, ConnectedBankAccount $connectedBankAccount): Account
15
+    {
16
+        if ($bankAccount->account()->doesntExist()) {
17
+            return $this->processNewAccount($bankAccount, $company, $connectedBankAccount);
18
+        }
19
+
20
+        return $bankAccount->account;
21
+    }
22
+
23
+    public function processNewAccount(BankAccount $bankAccount, Company $company, ConnectedBankAccount $connectedBankAccount): Account
24
+    {
25
+        $currencyCode = $connectedBankAccount->currency_code ?? 'USD';
26
+
27
+        $currency = $this->ensureCurrencyExists($company, $currencyCode);
28
+
29
+        $accountSubtype = $this->getAccountSubtype($bankAccount->type->value);
30
+
31
+        $accountSubtypeId = $this->resolveAccountSubtypeId($company, $accountSubtype);
32
+
33
+        return $bankAccount->account()->create([
34
+            'company_id' => $company->id,
35
+            'name' => $connectedBankAccount->name,
36
+            'currency_code' => $currency->code,
37
+            'description' => $connectedBankAccount->name,
38
+            'subtype_id' => $accountSubtypeId,
39
+            'active' => true,
40
+        ]);
41
+    }
42
+
43
+    protected function ensureCurrencyExists(Company $company, string $currencyCode): Currency
44
+    {
45
+        $currencyRelationship = $company->currencies();
46
+
47
+        $defaultCurrency = $currencyRelationship->firstWhere('enabled', true);
48
+
49
+        $hasDefaultCurrency = $defaultCurrency !== null;
50
+
51
+        $currency_code = currency($currencyCode);
52
+
53
+        return $currencyRelationship->firstOrCreate([
54
+            'code' => $currencyCode,
55
+        ], [
56
+            'name' => $currency_code->getName(),
57
+            'rate' => $currency_code->getRate(),
58
+            'precision' => $currency_code->getPrecision(),
59
+            'symbol' => $currency_code->getSymbol(),
60
+            'symbol_first' => $currency_code->isSymbolFirst(),
61
+            'decimal_mark' => $currency_code->getDecimalMark(),
62
+            'thousands_separator' => $currency_code->getThousandsSeparator(),
63
+            'enabled' => ! $hasDefaultCurrency,
64
+        ]);
65
+    }
66
+
67
+    protected function getAccountSubtype(string $plaidType): string
68
+    {
69
+        return match ($plaidType) {
70
+            'depository' => 'Cash and Cash Equivalents',
71
+            'credit' => 'Short-Term Borrowings',
72
+            'loan' => 'Long-Term Borrowings',
73
+            'investment' => 'Long-Term Investments',
74
+            'other' => 'Other Current Assets',
75
+        };
76
+    }
77
+
78
+    protected function resolveAccountSubtypeId(Company $company, string $accountSubtype): int
79
+    {
80
+        $accountSubtypeId = $company->accountSubtypes()
81
+            ->where('name', $accountSubtype)
82
+            ->value('id');
83
+
84
+        if ($accountSubtypeId === null) {
85
+            throw new ModelNotFoundException("Account subtype '{$accountSubtype}' not found for company '{$company->name}'");
86
+        }
87
+
88
+        return $accountSubtypeId;
89
+    }
90
+}

+ 30
- 0
app/Services/BankAccountService.php 查看文件

1
+<?php
2
+
3
+namespace App\Services;
4
+
5
+use App\Models\Banking\BankAccount;
6
+use App\Models\Banking\ConnectedBankAccount;
7
+use App\Models\Company;
8
+
9
+class BankAccountService
10
+{
11
+    public function getOrProcessBankAccount(Company $company, ConnectedBankAccount $connectedBankAccount, int | string $selectedBankAccountId): BankAccount
12
+    {
13
+        if ($selectedBankAccountId === 'new') {
14
+            return $this->processNewBankAccount($company, $connectedBankAccount);
15
+        }
16
+
17
+        return $company->bankAccounts()->find($selectedBankAccountId);
18
+    }
19
+
20
+    protected function processNewBankAccount(Company $company, ConnectedBankAccount $connectedBankAccount): BankAccount
21
+    {
22
+        return $connectedBankAccount->bankAccount()->create([
23
+            'company_id' => $company->id,
24
+            'institution_id' => $connectedBankAccount->institution_id,
25
+            'type' => $connectedBankAccount->type,
26
+            'number' => $connectedBankAccount->mask,
27
+            'enabled' => BankAccount::where('company_id', $company->id)->where('enabled', true)->doesntExist(),
28
+        ]);
29
+    }
30
+}

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

13
 
13
 
14
 class PlaidService
14
 class PlaidService
15
 {
15
 {
16
-    public const string API_VERSION = '2020-09-14';
16
+    public const API_VERSION = '2020-09-14';
17
 
17
 
18
     protected ?string $client_id;
18
     protected ?string $client_id;
19
 
19
 
194
         );
194
         );
195
     }
195
     }
196
 
196
 
197
-    public function createLinkToken(string $client_name, string $language, array $country_codes, array $user, array $products = []): object
197
+    public function createLinkToken(string $client_name, string $language, array $country_codes, array $user, array $products): object
198
     {
198
     {
199
         $data = [
199
         $data = [
200
             'client_name' => $client_name,
200
             'client_name' => $client_name,

+ 210
- 0
app/Services/TransactionService.php 查看文件

1
+<?php
2
+
3
+namespace App\Services;
4
+
5
+use App\Enums\Accounting\AccountCategory;
6
+use App\Enums\Accounting\AccountType;
7
+use App\Models\Accounting\Account;
8
+use App\Models\Accounting\Transaction;
9
+use App\Models\Banking\ConnectedBankAccount;
10
+use App\Models\Company;
11
+use App\Models\Setting\Category;
12
+use App\Scopes\CurrentCompanyScope;
13
+use Illuminate\Support\Carbon;
14
+
15
+class TransactionService
16
+{
17
+    public function createStartingBalanceIfNeeded(Company $company, Account $account, ConnectedBankAccount $connectedBankAccount, array $transactions, float $currentBalance, string $startDate): void
18
+    {
19
+        if ($account->transactions()->withoutGlobalScope(CurrentCompanyScope::class)->doesntExist()) {
20
+            $accountSign = $account->category === AccountCategory::Asset ? 1 : -1;
21
+
22
+            $sumOfTransactions = collect($transactions)->reduce(static function ($carry, $transaction) {
23
+                return bcadd($carry, (string) -$transaction->amount, 2);
24
+            }, '0.00');
25
+
26
+            $adjustedBalance = (string) ($currentBalance * $accountSign);
27
+
28
+            $startingBalance = bcsub($adjustedBalance, $sumOfTransactions, 2);
29
+
30
+            $this->createStartingBalanceTransaction($company, $account, $connectedBankAccount, (float) $startingBalance, $startDate);
31
+        }
32
+    }
33
+
34
+    public function storeTransactions(Company $company, Account $account, ConnectedBankAccount $connectedBankAccount, array $transactions): void
35
+    {
36
+        foreach ($transactions as $transaction) {
37
+            $this->storeTransaction($company, $account, $connectedBankAccount, $transaction);
38
+        }
39
+    }
40
+
41
+    public function createStartingBalanceTransaction(Company $company, Account $account, ConnectedBankAccount $connectedBankAccount, float $startingBalance, string $startDate): void
42
+    {
43
+        [$transactionType, $method] = $startingBalance >= 0 ? ['income', 'deposit'] : ['expense', 'withdrawal'];
44
+        $category = $this->getUncategorizedCategory($company, $transactionType);
45
+        $chartAccount = $account->where('category', AccountCategory::Equity)->where('name', 'Owner\'s Equity')->first();
46
+
47
+        $transactionRecord = $account->transactions()->create([
48
+            'company_id' => $company->id,
49
+            'category_id' => $category->id,
50
+            'bank_account_id' => $connectedBankAccount->bank_account_id,
51
+            'type' => $transactionType,
52
+            'amount' => abs($startingBalance),
53
+            'method' => $method,
54
+            'payment_channel' => 'other',
55
+            'posted_at' => $startDate,
56
+            'description' => 'Starting Balance',
57
+            'pending' => false,
58
+            'reviewed' => true,
59
+        ]);
60
+
61
+        $this->createJournalEntries($company, $account, $transactionRecord, $chartAccount);
62
+    }
63
+
64
+    public function storeTransaction(Company $company, Account $account, ConnectedBankAccount $connectedBankAccount, object $transaction): void
65
+    {
66
+        [$transactionType, $method] = $transaction->amount < 0 ? ['income', 'deposit'] : ['expense', 'withdrawal'];
67
+        $paymentChannel = $transaction->payment_channel;
68
+        $category = $this->getCategoryFromTransaction($company, $transaction, $transactionType);
69
+        $chartAccount = $category->account ?? $this->getChartFromTransaction($company, $transactionType);
70
+        $postedAt = $transaction->datetime ?? Carbon::parse($transaction->date)->toDateTimeString();
71
+        $description = $transaction->name;
72
+
73
+        $transactionRecord = $account->transactions()->create([
74
+            'company_id' => $company->id,
75
+            'category_id' => $category->id,
76
+            'bank_account_id' => $connectedBankAccount->bank_account_id,
77
+            'type' => $transactionType,
78
+            'amount' => abs($transaction->amount),
79
+            'method' => $method,
80
+            'payment_channel' => $paymentChannel,
81
+            'posted_at' => $postedAt,
82
+            'description' => $description,
83
+            'pending' => $transaction->pending,
84
+            'reviewed' => false,
85
+        ]);
86
+
87
+        $this->createJournalEntries($company, $account, $transactionRecord, $chartAccount);
88
+    }
89
+
90
+    public function createJournalEntries(Company $company, Account $account, Transaction $transaction, Account $chartAccount): void
91
+    {
92
+        $debitAccount = $transaction->type === 'expense' ? $chartAccount : $account;
93
+        $creditAccount = $transaction->type === 'expense' ? $account : $chartAccount;
94
+
95
+        $amount = $transaction->amount;
96
+
97
+        $debitAccount->journalEntries()->create([
98
+            'company_id' => $company->id,
99
+            'transaction_id' => $transaction->id,
100
+            'type' => 'debit',
101
+            'amount' => $amount,
102
+            'description' => $transaction->description,
103
+        ]);
104
+
105
+        $creditAccount->journalEntries()->create([
106
+            'company_id' => $company->id,
107
+            'transaction_id' => $transaction->id,
108
+            'type' => 'credit',
109
+            'amount' => $amount,
110
+            'description' => $transaction->description,
111
+        ]);
112
+    }
113
+
114
+    public function getCategoryFromTransaction(Company $company, object $transaction, string $transactionType): Category
115
+    {
116
+        $companyCategories = $company->categories()
117
+            ->where('type', $transactionType)
118
+            ->whereNotIn('name', ['Other Income', 'Other Expense'])
119
+            ->get();
120
+
121
+        $bestMatchName = $this->findBestCategoryMatch($transaction, $companyCategories->pluck('name')->toArray());
122
+
123
+        if ($bestMatchName === null) {
124
+            return $this->getUncategorizedCategory($company, $transactionType);
125
+        }
126
+
127
+        $category = $companyCategories->firstWhere('name', $bestMatchName);
128
+
129
+        return $category ?: $this->getUncategorizedCategory($company, $transactionType);
130
+    }
131
+
132
+    private function findBestCategoryMatch(object $transaction, array $userCategories): ?string
133
+    {
134
+        $acceptableConfidenceLevels = ['VERY_HIGH', 'HIGH'];
135
+        $similarityThreshold = 0.7;
136
+        $plaidDetail = $transaction->personal_finance_category->detailed ?? null;
137
+        $plaidPrimary = $transaction->personal_finance_category->primary ?? null;
138
+        $bestMatchName = null;
139
+        $bestMatchPercent = 0.0;
140
+
141
+        foreach ([$plaidDetail, $plaidPrimary] as $plaidCategory) {
142
+            if ($plaidCategory !== null && in_array($transaction->personal_finance_category->confidence_level, $acceptableConfidenceLevels, true)) {
143
+                $currentMatchPercent = 0.0;
144
+                $matchedName = $this->closestCategory($plaidCategory, $userCategories, $currentMatchPercent);
145
+                if ($currentMatchPercent >= $similarityThreshold && $currentMatchPercent > $bestMatchPercent) {
146
+                    $bestMatchPercent = $currentMatchPercent;
147
+                    $bestMatchName = $matchedName;
148
+                }
149
+            }
150
+        }
151
+
152
+        return $bestMatchName;
153
+    }
154
+
155
+    public function closestCategory(string $input, array $categories, ?float &$percent): ?string
156
+    {
157
+        $inputNormalized = strtolower(str_replace('_', ' ', $input));
158
+        $originalToNormalized = [];
159
+        foreach ($categories as $originalCategory) {
160
+            $normalizedCategory = strtolower(str_replace('_', ' ', $originalCategory));
161
+            $originalToNormalized[$normalizedCategory] = $originalCategory;
162
+        }
163
+
164
+        $shortest = -1;
165
+        $closestNormalized = null;
166
+        foreach ($originalToNormalized as $normalizedCategory => $originalCategory) {
167
+            $lev = levenshtein($inputNormalized, $normalizedCategory);
168
+            if ($lev === 0 || $lev < $shortest || $shortest < 0) {
169
+                $closestNormalized = $normalizedCategory;
170
+                $shortest = $lev;
171
+            }
172
+        }
173
+
174
+        if ($closestNormalized !== null) {
175
+            $percent = 1.0 - ($shortest / max(strlen($inputNormalized), strlen($closestNormalized)));
176
+
177
+            return $originalToNormalized[$closestNormalized]; // return the original category name
178
+        }
179
+
180
+        $percent = 0.0;
181
+
182
+        return null;
183
+    }
184
+
185
+    public function getUncategorizedCategory(Company $company, string $transactionType): Category
186
+    {
187
+        $name = match ($transactionType) {
188
+            'income' => 'Other Income',
189
+            'expense' => 'Other Expense',
190
+        };
191
+
192
+        return $company->categories()
193
+            ->where('type', $transactionType)
194
+            ->where('name', $name)
195
+            ->firstOrFail();
196
+    }
197
+
198
+    public function getChartFromTransaction(Company $company, string $transactionType): Account
199
+    {
200
+        [$type, $name] = match ($transactionType) {
201
+            'income' => [AccountType::UncategorizedRevenue, 'Uncategorized Income'],
202
+            'expense' => [AccountType::UncategorizedExpense, 'Uncategorized Expense'],
203
+        };
204
+
205
+        return $company->accounts()
206
+            ->where('type', $type)
207
+            ->where('name', $name)
208
+            ->firstOrFail();
209
+    }
210
+}

+ 141
- 141
composer.lock 查看文件

413
         },
413
         },
414
         {
414
         {
415
             "name": "aws/aws-sdk-php",
415
             "name": "aws/aws-sdk-php",
416
-            "version": "3.299.1",
416
+            "version": "3.300.4",
417
             "source": {
417
             "source": {
418
                 "type": "git",
418
                 "type": "git",
419
                 "url": "https://github.com/aws/aws-sdk-php.git",
419
                 "url": "https://github.com/aws/aws-sdk-php.git",
420
-                "reference": "a0f87b8e8bfb9afd0ffd702fcda556b465eee457"
420
+                "reference": "27d59c22c121ce9c0041c563dc9d7270e180925c"
421
             },
421
             },
422
             "dist": {
422
             "dist": {
423
                 "type": "zip",
423
                 "type": "zip",
424
-                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a0f87b8e8bfb9afd0ffd702fcda556b465eee457",
425
-                "reference": "a0f87b8e8bfb9afd0ffd702fcda556b465eee457",
424
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/27d59c22c121ce9c0041c563dc9d7270e180925c",
425
+                "reference": "27d59c22c121ce9c0041c563dc9d7270e180925c",
426
                 "shasum": ""
426
                 "shasum": ""
427
             },
427
             },
428
             "require": {
428
             "require": {
502
             "support": {
502
             "support": {
503
                 "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
503
                 "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
504
                 "issues": "https://github.com/aws/aws-sdk-php/issues",
504
                 "issues": "https://github.com/aws/aws-sdk-php/issues",
505
-                "source": "https://github.com/aws/aws-sdk-php/tree/3.299.1"
505
+                "source": "https://github.com/aws/aws-sdk-php/tree/3.300.4"
506
             },
506
             },
507
-            "time": "2024-02-16T19:08:34+00:00"
507
+            "time": "2024-02-23T19:10:30+00:00"
508
         },
508
         },
509
         {
509
         {
510
             "name": "aws/aws-sdk-php-laravel",
510
             "name": "aws/aws-sdk-php-laravel",
1757
         },
1757
         },
1758
         {
1758
         {
1759
             "name": "filament/actions",
1759
             "name": "filament/actions",
1760
-            "version": "v3.2.34",
1760
+            "version": "v3.2.37",
1761
             "source": {
1761
             "source": {
1762
                 "type": "git",
1762
                 "type": "git",
1763
                 "url": "https://github.com/filamentphp/actions.git",
1763
                 "url": "https://github.com/filamentphp/actions.git",
1764
-                "reference": "db3f17e5a6c550e98f8246bc2ca9c882fe1bb0e2"
1764
+                "reference": "3e369b846363b990a12b70eb364a37c16100f3cb"
1765
             },
1765
             },
1766
             "dist": {
1766
             "dist": {
1767
                 "type": "zip",
1767
                 "type": "zip",
1768
-                "url": "https://api.github.com/repos/filamentphp/actions/zipball/db3f17e5a6c550e98f8246bc2ca9c882fe1bb0e2",
1769
-                "reference": "db3f17e5a6c550e98f8246bc2ca9c882fe1bb0e2",
1768
+                "url": "https://api.github.com/repos/filamentphp/actions/zipball/3e369b846363b990a12b70eb364a37c16100f3cb",
1769
+                "reference": "3e369b846363b990a12b70eb364a37c16100f3cb",
1770
                 "shasum": ""
1770
                 "shasum": ""
1771
             },
1771
             },
1772
             "require": {
1772
             "require": {
1775
                 "filament/infolists": "self.version",
1775
                 "filament/infolists": "self.version",
1776
                 "filament/notifications": "self.version",
1776
                 "filament/notifications": "self.version",
1777
                 "filament/support": "self.version",
1777
                 "filament/support": "self.version",
1778
-                "illuminate/contracts": "^10.0",
1779
-                "illuminate/database": "^10.0",
1780
-                "illuminate/support": "^10.0",
1778
+                "illuminate/contracts": "^10.45",
1779
+                "illuminate/database": "^10.45",
1780
+                "illuminate/support": "^10.45",
1781
                 "league/csv": "^9.14",
1781
                 "league/csv": "^9.14",
1782
                 "openspout/openspout": "^4.23",
1782
                 "openspout/openspout": "^4.23",
1783
                 "php": "^8.1",
1783
                 "php": "^8.1",
1806
                 "issues": "https://github.com/filamentphp/filament/issues",
1806
                 "issues": "https://github.com/filamentphp/filament/issues",
1807
                 "source": "https://github.com/filamentphp/filament"
1807
                 "source": "https://github.com/filamentphp/filament"
1808
             },
1808
             },
1809
-            "time": "2024-02-12T17:10:13+00:00"
1809
+            "time": "2024-02-23T22:25:58+00:00"
1810
         },
1810
         },
1811
         {
1811
         {
1812
             "name": "filament/filament",
1812
             "name": "filament/filament",
1813
-            "version": "v3.2.34",
1813
+            "version": "v3.2.37",
1814
             "source": {
1814
             "source": {
1815
                 "type": "git",
1815
                 "type": "git",
1816
                 "url": "https://github.com/filamentphp/panels.git",
1816
                 "url": "https://github.com/filamentphp/panels.git",
1817
-                "reference": "275758cf9a0dea16214a89f8185a992d4b9cafea"
1817
+                "reference": "2fbe4acc73d0296e7894aeffe518c7080dab0cc6"
1818
             },
1818
             },
1819
             "dist": {
1819
             "dist": {
1820
                 "type": "zip",
1820
                 "type": "zip",
1821
-                "url": "https://api.github.com/repos/filamentphp/panels/zipball/275758cf9a0dea16214a89f8185a992d4b9cafea",
1822
-                "reference": "275758cf9a0dea16214a89f8185a992d4b9cafea",
1821
+                "url": "https://api.github.com/repos/filamentphp/panels/zipball/2fbe4acc73d0296e7894aeffe518c7080dab0cc6",
1822
+                "reference": "2fbe4acc73d0296e7894aeffe518c7080dab0cc6",
1823
                 "shasum": ""
1823
                 "shasum": ""
1824
             },
1824
             },
1825
             "require": {
1825
             "require": {
1831
                 "filament/support": "self.version",
1831
                 "filament/support": "self.version",
1832
                 "filament/tables": "self.version",
1832
                 "filament/tables": "self.version",
1833
                 "filament/widgets": "self.version",
1833
                 "filament/widgets": "self.version",
1834
-                "illuminate/auth": "^10.0",
1835
-                "illuminate/console": "^10.0",
1836
-                "illuminate/contracts": "^10.0",
1837
-                "illuminate/cookie": "^10.0",
1838
-                "illuminate/database": "^10.0",
1839
-                "illuminate/http": "^10.0",
1840
-                "illuminate/routing": "^10.0",
1841
-                "illuminate/session": "^10.0",
1842
-                "illuminate/support": "^10.0",
1843
-                "illuminate/view": "^10.0",
1834
+                "illuminate/auth": "^10.45",
1835
+                "illuminate/console": "^10.45",
1836
+                "illuminate/contracts": "^10.45",
1837
+                "illuminate/cookie": "^10.45",
1838
+                "illuminate/database": "^10.45",
1839
+                "illuminate/http": "^10.45",
1840
+                "illuminate/routing": "^10.45",
1841
+                "illuminate/session": "^10.45",
1842
+                "illuminate/support": "^10.45",
1843
+                "illuminate/view": "^10.45",
1844
                 "php": "^8.1",
1844
                 "php": "^8.1",
1845
                 "spatie/laravel-package-tools": "^1.9"
1845
                 "spatie/laravel-package-tools": "^1.9"
1846
             },
1846
             },
1871
                 "issues": "https://github.com/filamentphp/filament/issues",
1871
                 "issues": "https://github.com/filamentphp/filament/issues",
1872
                 "source": "https://github.com/filamentphp/filament"
1872
                 "source": "https://github.com/filamentphp/filament"
1873
             },
1873
             },
1874
-            "time": "2024-02-13T11:56:58+00:00"
1874
+            "time": "2024-02-23T22:26:19+00:00"
1875
         },
1875
         },
1876
         {
1876
         {
1877
             "name": "filament/forms",
1877
             "name": "filament/forms",
1878
-            "version": "v3.2.34",
1878
+            "version": "v3.2.37",
1879
             "source": {
1879
             "source": {
1880
                 "type": "git",
1880
                 "type": "git",
1881
                 "url": "https://github.com/filamentphp/forms.git",
1881
                 "url": "https://github.com/filamentphp/forms.git",
1882
-                "reference": "2c1772bba223fee30b855870059b111bb2e465a9"
1882
+                "reference": "403a104fa76c663ee6252f0bcafb0bf5435f6e4a"
1883
             },
1883
             },
1884
             "dist": {
1884
             "dist": {
1885
                 "type": "zip",
1885
                 "type": "zip",
1886
-                "url": "https://api.github.com/repos/filamentphp/forms/zipball/2c1772bba223fee30b855870059b111bb2e465a9",
1887
-                "reference": "2c1772bba223fee30b855870059b111bb2e465a9",
1886
+                "url": "https://api.github.com/repos/filamentphp/forms/zipball/403a104fa76c663ee6252f0bcafb0bf5435f6e4a",
1887
+                "reference": "403a104fa76c663ee6252f0bcafb0bf5435f6e4a",
1888
                 "shasum": ""
1888
                 "shasum": ""
1889
             },
1889
             },
1890
             "require": {
1890
             "require": {
1891
                 "danharrin/date-format-converter": "^0.3",
1891
                 "danharrin/date-format-converter": "^0.3",
1892
                 "filament/actions": "self.version",
1892
                 "filament/actions": "self.version",
1893
                 "filament/support": "self.version",
1893
                 "filament/support": "self.version",
1894
-                "illuminate/console": "^10.0",
1895
-                "illuminate/contracts": "^10.0",
1896
-                "illuminate/database": "^10.0",
1897
-                "illuminate/filesystem": "^10.0",
1898
-                "illuminate/support": "^10.0",
1899
-                "illuminate/validation": "^10.0",
1900
-                "illuminate/view": "^10.0",
1894
+                "illuminate/console": "^10.45",
1895
+                "illuminate/contracts": "^10.45",
1896
+                "illuminate/database": "^10.45",
1897
+                "illuminate/filesystem": "^10.45",
1898
+                "illuminate/support": "^10.45",
1899
+                "illuminate/validation": "^10.45",
1900
+                "illuminate/view": "^10.45",
1901
                 "php": "^8.1",
1901
                 "php": "^8.1",
1902
                 "spatie/laravel-package-tools": "^1.9"
1902
                 "spatie/laravel-package-tools": "^1.9"
1903
             },
1903
             },
1927
                 "issues": "https://github.com/filamentphp/filament/issues",
1927
                 "issues": "https://github.com/filamentphp/filament/issues",
1928
                 "source": "https://github.com/filamentphp/filament"
1928
                 "source": "https://github.com/filamentphp/filament"
1929
             },
1929
             },
1930
-            "time": "2024-02-11T20:52:55+00:00"
1930
+            "time": "2024-02-23T22:26:00+00:00"
1931
         },
1931
         },
1932
         {
1932
         {
1933
             "name": "filament/infolists",
1933
             "name": "filament/infolists",
1934
-            "version": "v3.2.34",
1934
+            "version": "v3.2.37",
1935
             "source": {
1935
             "source": {
1936
                 "type": "git",
1936
                 "type": "git",
1937
                 "url": "https://github.com/filamentphp/infolists.git",
1937
                 "url": "https://github.com/filamentphp/infolists.git",
1938
-                "reference": "0af24d86945ed91dbd9cb48cff67b61e06d5913e"
1938
+                "reference": "a645711cbc95fd9061f21300f37edc0e8960a744"
1939
             },
1939
             },
1940
             "dist": {
1940
             "dist": {
1941
                 "type": "zip",
1941
                 "type": "zip",
1942
-                "url": "https://api.github.com/repos/filamentphp/infolists/zipball/0af24d86945ed91dbd9cb48cff67b61e06d5913e",
1943
-                "reference": "0af24d86945ed91dbd9cb48cff67b61e06d5913e",
1942
+                "url": "https://api.github.com/repos/filamentphp/infolists/zipball/a645711cbc95fd9061f21300f37edc0e8960a744",
1943
+                "reference": "a645711cbc95fd9061f21300f37edc0e8960a744",
1944
                 "shasum": ""
1944
                 "shasum": ""
1945
             },
1945
             },
1946
             "require": {
1946
             "require": {
1947
                 "filament/actions": "self.version",
1947
                 "filament/actions": "self.version",
1948
                 "filament/support": "self.version",
1948
                 "filament/support": "self.version",
1949
-                "illuminate/console": "^10.0",
1950
-                "illuminate/contracts": "^10.0",
1951
-                "illuminate/database": "^10.0",
1952
-                "illuminate/filesystem": "^10.0",
1953
-                "illuminate/support": "^10.0",
1954
-                "illuminate/view": "^10.0",
1949
+                "illuminate/console": "^10.45",
1950
+                "illuminate/contracts": "^10.45",
1951
+                "illuminate/database": "^10.45",
1952
+                "illuminate/filesystem": "^10.45",
1953
+                "illuminate/support": "^10.45",
1954
+                "illuminate/view": "^10.45",
1955
                 "php": "^8.1",
1955
                 "php": "^8.1",
1956
                 "spatie/laravel-package-tools": "^1.9"
1956
                 "spatie/laravel-package-tools": "^1.9"
1957
             },
1957
             },
1978
                 "issues": "https://github.com/filamentphp/filament/issues",
1978
                 "issues": "https://github.com/filamentphp/filament/issues",
1979
                 "source": "https://github.com/filamentphp/filament"
1979
                 "source": "https://github.com/filamentphp/filament"
1980
             },
1980
             },
1981
-            "time": "2024-02-09T17:40:48+00:00"
1981
+            "time": "2024-02-23T22:26:11+00:00"
1982
         },
1982
         },
1983
         {
1983
         {
1984
             "name": "filament/notifications",
1984
             "name": "filament/notifications",
1985
-            "version": "v3.2.34",
1985
+            "version": "v3.2.37",
1986
             "source": {
1986
             "source": {
1987
                 "type": "git",
1987
                 "type": "git",
1988
                 "url": "https://github.com/filamentphp/notifications.git",
1988
                 "url": "https://github.com/filamentphp/notifications.git",
1989
-                "reference": "4786bb652bf6d64a9e68cf8302cf761f8dff3c4b"
1989
+                "reference": "41f3b06547c8ea204049ebee296a6f42bc9be086"
1990
             },
1990
             },
1991
             "dist": {
1991
             "dist": {
1992
                 "type": "zip",
1992
                 "type": "zip",
1993
-                "url": "https://api.github.com/repos/filamentphp/notifications/zipball/4786bb652bf6d64a9e68cf8302cf761f8dff3c4b",
1994
-                "reference": "4786bb652bf6d64a9e68cf8302cf761f8dff3c4b",
1993
+                "url": "https://api.github.com/repos/filamentphp/notifications/zipball/41f3b06547c8ea204049ebee296a6f42bc9be086",
1994
+                "reference": "41f3b06547c8ea204049ebee296a6f42bc9be086",
1995
                 "shasum": ""
1995
                 "shasum": ""
1996
             },
1996
             },
1997
             "require": {
1997
             "require": {
1998
                 "filament/actions": "self.version",
1998
                 "filament/actions": "self.version",
1999
                 "filament/support": "self.version",
1999
                 "filament/support": "self.version",
2000
-                "illuminate/contracts": "^10.0",
2001
-                "illuminate/filesystem": "^10.0",
2002
-                "illuminate/notifications": "^10.0",
2003
-                "illuminate/support": "^10.0",
2000
+                "illuminate/contracts": "^10.45",
2001
+                "illuminate/filesystem": "^10.45",
2002
+                "illuminate/notifications": "^10.45",
2003
+                "illuminate/support": "^10.45",
2004
                 "php": "^8.1",
2004
                 "php": "^8.1",
2005
                 "spatie/laravel-package-tools": "^1.9"
2005
                 "spatie/laravel-package-tools": "^1.9"
2006
             },
2006
             },
2030
                 "issues": "https://github.com/filamentphp/filament/issues",
2030
                 "issues": "https://github.com/filamentphp/filament/issues",
2031
                 "source": "https://github.com/filamentphp/filament"
2031
                 "source": "https://github.com/filamentphp/filament"
2032
             },
2032
             },
2033
-            "time": "2024-02-07T18:46:44+00:00"
2033
+            "time": "2024-02-23T22:26:08+00:00"
2034
         },
2034
         },
2035
         {
2035
         {
2036
             "name": "filament/spatie-laravel-tags-plugin",
2036
             "name": "filament/spatie-laravel-tags-plugin",
2037
-            "version": "v3.2.34",
2037
+            "version": "v3.2.37",
2038
             "source": {
2038
             "source": {
2039
                 "type": "git",
2039
                 "type": "git",
2040
                 "url": "https://github.com/filamentphp/spatie-laravel-tags-plugin.git",
2040
                 "url": "https://github.com/filamentphp/spatie-laravel-tags-plugin.git",
2041
-                "reference": "69c933b272a0d39632d2a706f566a645638c3969"
2041
+                "reference": "76c990bbed0113a9d819b5ce57dbe1b970433927"
2042
             },
2042
             },
2043
             "dist": {
2043
             "dist": {
2044
                 "type": "zip",
2044
                 "type": "zip",
2045
-                "url": "https://api.github.com/repos/filamentphp/spatie-laravel-tags-plugin/zipball/69c933b272a0d39632d2a706f566a645638c3969",
2046
-                "reference": "69c933b272a0d39632d2a706f566a645638c3969",
2045
+                "url": "https://api.github.com/repos/filamentphp/spatie-laravel-tags-plugin/zipball/76c990bbed0113a9d819b5ce57dbe1b970433927",
2046
+                "reference": "76c990bbed0113a9d819b5ce57dbe1b970433927",
2047
                 "shasum": ""
2047
                 "shasum": ""
2048
             },
2048
             },
2049
             "require": {
2049
             "require": {
2050
-                "illuminate/database": "^10.0",
2050
+                "illuminate/database": "^10.45",
2051
                 "php": "^8.1",
2051
                 "php": "^8.1",
2052
                 "spatie/laravel-tags": "^4.0"
2052
                 "spatie/laravel-tags": "^4.0"
2053
             },
2053
             },
2067
                 "issues": "https://github.com/filamentphp/filament/issues",
2067
                 "issues": "https://github.com/filamentphp/filament/issues",
2068
                 "source": "https://github.com/filamentphp/filament"
2068
                 "source": "https://github.com/filamentphp/filament"
2069
             },
2069
             },
2070
-            "time": "2024-01-29T13:20:09+00:00"
2070
+            "time": "2024-02-23T22:26:28+00:00"
2071
         },
2071
         },
2072
         {
2072
         {
2073
             "name": "filament/support",
2073
             "name": "filament/support",
2074
-            "version": "v3.2.34",
2074
+            "version": "v3.2.37",
2075
             "source": {
2075
             "source": {
2076
                 "type": "git",
2076
                 "type": "git",
2077
                 "url": "https://github.com/filamentphp/support.git",
2077
                 "url": "https://github.com/filamentphp/support.git",
2078
-                "reference": "c2be482587352c21bd9dc215b2e489c3598a9a06"
2078
+                "reference": "b9283eca3b999c35afc2e3cb0d69f6acb77e011a"
2079
             },
2079
             },
2080
             "dist": {
2080
             "dist": {
2081
                 "type": "zip",
2081
                 "type": "zip",
2082
-                "url": "https://api.github.com/repos/filamentphp/support/zipball/c2be482587352c21bd9dc215b2e489c3598a9a06",
2083
-                "reference": "c2be482587352c21bd9dc215b2e489c3598a9a06",
2082
+                "url": "https://api.github.com/repos/filamentphp/support/zipball/b9283eca3b999c35afc2e3cb0d69f6acb77e011a",
2083
+                "reference": "b9283eca3b999c35afc2e3cb0d69f6acb77e011a",
2084
                 "shasum": ""
2084
                 "shasum": ""
2085
             },
2085
             },
2086
             "require": {
2086
             "require": {
2087
                 "blade-ui-kit/blade-heroicons": "^2.2.1",
2087
                 "blade-ui-kit/blade-heroicons": "^2.2.1",
2088
                 "doctrine/dbal": "^3.2",
2088
                 "doctrine/dbal": "^3.2",
2089
                 "ext-intl": "*",
2089
                 "ext-intl": "*",
2090
-                "illuminate/contracts": "^10.0",
2091
-                "illuminate/support": "^10.0",
2092
-                "illuminate/view": "^10.0",
2090
+                "illuminate/contracts": "^10.45",
2091
+                "illuminate/support": "^10.45",
2092
+                "illuminate/view": "^10.45",
2093
                 "livewire/livewire": "^3.2.3",
2093
                 "livewire/livewire": "^3.2.3",
2094
                 "php": "^8.1",
2094
                 "php": "^8.1",
2095
                 "ryangjchandler/blade-capture-directive": "^0.2|^0.3",
2095
                 "ryangjchandler/blade-capture-directive": "^0.2|^0.3",
2124
                 "issues": "https://github.com/filamentphp/filament/issues",
2124
                 "issues": "https://github.com/filamentphp/filament/issues",
2125
                 "source": "https://github.com/filamentphp/filament"
2125
                 "source": "https://github.com/filamentphp/filament"
2126
             },
2126
             },
2127
-            "time": "2024-02-13T06:30:24+00:00"
2127
+            "time": "2024-02-24T06:52:20+00:00"
2128
         },
2128
         },
2129
         {
2129
         {
2130
             "name": "filament/tables",
2130
             "name": "filament/tables",
2131
-            "version": "v3.2.34",
2131
+            "version": "v3.2.37",
2132
             "source": {
2132
             "source": {
2133
                 "type": "git",
2133
                 "type": "git",
2134
                 "url": "https://github.com/filamentphp/tables.git",
2134
                 "url": "https://github.com/filamentphp/tables.git",
2135
-                "reference": "684cd0e83203f2ffc43f38451fd5a87df8f0c944"
2135
+                "reference": "242a3d6e99bc095b225a145e362e07a3fb92fed1"
2136
             },
2136
             },
2137
             "dist": {
2137
             "dist": {
2138
                 "type": "zip",
2138
                 "type": "zip",
2139
-                "url": "https://api.github.com/repos/filamentphp/tables/zipball/684cd0e83203f2ffc43f38451fd5a87df8f0c944",
2140
-                "reference": "684cd0e83203f2ffc43f38451fd5a87df8f0c944",
2139
+                "url": "https://api.github.com/repos/filamentphp/tables/zipball/242a3d6e99bc095b225a145e362e07a3fb92fed1",
2140
+                "reference": "242a3d6e99bc095b225a145e362e07a3fb92fed1",
2141
                 "shasum": ""
2141
                 "shasum": ""
2142
             },
2142
             },
2143
             "require": {
2143
             "require": {
2144
                 "filament/actions": "self.version",
2144
                 "filament/actions": "self.version",
2145
                 "filament/forms": "self.version",
2145
                 "filament/forms": "self.version",
2146
                 "filament/support": "self.version",
2146
                 "filament/support": "self.version",
2147
-                "illuminate/console": "^10.0",
2148
-                "illuminate/contracts": "^10.0",
2149
-                "illuminate/database": "^10.0",
2150
-                "illuminate/filesystem": "^10.0",
2151
-                "illuminate/support": "^10.0",
2152
-                "illuminate/view": "^10.0",
2147
+                "illuminate/console": "^10.45",
2148
+                "illuminate/contracts": "^10.45",
2149
+                "illuminate/database": "^10.45",
2150
+                "illuminate/filesystem": "^10.45",
2151
+                "illuminate/support": "^10.45",
2152
+                "illuminate/view": "^10.45",
2153
                 "kirschbaum-development/eloquent-power-joins": "^3.0",
2153
                 "kirschbaum-development/eloquent-power-joins": "^3.0",
2154
                 "php": "^8.1",
2154
                 "php": "^8.1",
2155
                 "spatie/laravel-package-tools": "^1.9"
2155
                 "spatie/laravel-package-tools": "^1.9"
2177
                 "issues": "https://github.com/filamentphp/filament/issues",
2177
                 "issues": "https://github.com/filamentphp/filament/issues",
2178
                 "source": "https://github.com/filamentphp/filament"
2178
                 "source": "https://github.com/filamentphp/filament"
2179
             },
2179
             },
2180
-            "time": "2024-02-13T11:57:12+00:00"
2180
+            "time": "2024-02-23T22:26:31+00:00"
2181
         },
2181
         },
2182
         {
2182
         {
2183
             "name": "filament/widgets",
2183
             "name": "filament/widgets",
2184
-            "version": "v3.2.34",
2184
+            "version": "v3.2.37",
2185
             "source": {
2185
             "source": {
2186
                 "type": "git",
2186
                 "type": "git",
2187
                 "url": "https://github.com/filamentphp/widgets.git",
2187
                 "url": "https://github.com/filamentphp/widgets.git",
2831
         },
2831
         },
2832
         {
2832
         {
2833
             "name": "laravel/framework",
2833
             "name": "laravel/framework",
2834
-            "version": "v10.44.0",
2834
+            "version": "v10.45.1",
2835
             "source": {
2835
             "source": {
2836
                 "type": "git",
2836
                 "type": "git",
2837
                 "url": "https://github.com/laravel/framework.git",
2837
                 "url": "https://github.com/laravel/framework.git",
2838
-                "reference": "1199dbe361787bbe9648131a79f53921b4148cf6"
2838
+                "reference": "dcf5d1d722b84ad38a5e053289130b6962f830bd"
2839
             },
2839
             },
2840
             "dist": {
2840
             "dist": {
2841
                 "type": "zip",
2841
                 "type": "zip",
2842
-                "url": "https://api.github.com/repos/laravel/framework/zipball/1199dbe361787bbe9648131a79f53921b4148cf6",
2843
-                "reference": "1199dbe361787bbe9648131a79f53921b4148cf6",
2842
+                "url": "https://api.github.com/repos/laravel/framework/zipball/dcf5d1d722b84ad38a5e053289130b6962f830bd",
2843
+                "reference": "dcf5d1d722b84ad38a5e053289130b6962f830bd",
2844
                 "shasum": ""
2844
                 "shasum": ""
2845
             },
2845
             },
2846
             "require": {
2846
             "require": {
3033
                 "issues": "https://github.com/laravel/framework/issues",
3033
                 "issues": "https://github.com/laravel/framework/issues",
3034
                 "source": "https://github.com/laravel/framework"
3034
                 "source": "https://github.com/laravel/framework"
3035
             },
3035
             },
3036
-            "time": "2024-02-13T16:01:16+00:00"
3036
+            "time": "2024-02-21T14:07:36+00:00"
3037
         },
3037
         },
3038
         {
3038
         {
3039
             "name": "laravel/prompts",
3039
             "name": "laravel/prompts",
3544
         },
3544
         },
3545
         {
3545
         {
3546
             "name": "league/csv",
3546
             "name": "league/csv",
3547
-            "version": "9.14.0",
3547
+            "version": "9.15.0",
3548
             "source": {
3548
             "source": {
3549
                 "type": "git",
3549
                 "type": "git",
3550
                 "url": "https://github.com/thephpleague/csv.git",
3550
                 "url": "https://github.com/thephpleague/csv.git",
3551
-                "reference": "34bf0df7340b60824b9449b5c526fcc3325070d5"
3551
+                "reference": "fa7e2441c0bc9b2360f4314fd6c954f7ff40d435"
3552
             },
3552
             },
3553
             "dist": {
3553
             "dist": {
3554
                 "type": "zip",
3554
                 "type": "zip",
3555
-                "url": "https://api.github.com/repos/thephpleague/csv/zipball/34bf0df7340b60824b9449b5c526fcc3325070d5",
3556
-                "reference": "34bf0df7340b60824b9449b5c526fcc3325070d5",
3555
+                "url": "https://api.github.com/repos/thephpleague/csv/zipball/fa7e2441c0bc9b2360f4314fd6c954f7ff40d435",
3556
+                "reference": "fa7e2441c0bc9b2360f4314fd6c954f7ff40d435",
3557
                 "shasum": ""
3557
                 "shasum": ""
3558
             },
3558
             },
3559
             "require": {
3559
             "require": {
3568
                 "ext-xdebug": "*",
3568
                 "ext-xdebug": "*",
3569
                 "friendsofphp/php-cs-fixer": "^v3.22.0",
3569
                 "friendsofphp/php-cs-fixer": "^v3.22.0",
3570
                 "phpbench/phpbench": "^1.2.15",
3570
                 "phpbench/phpbench": "^1.2.15",
3571
-                "phpstan/phpstan": "^1.10.50",
3571
+                "phpstan/phpstan": "^1.10.57",
3572
                 "phpstan/phpstan-deprecation-rules": "^1.1.4",
3572
                 "phpstan/phpstan-deprecation-rules": "^1.1.4",
3573
                 "phpstan/phpstan-phpunit": "^1.3.15",
3573
                 "phpstan/phpstan-phpunit": "^1.3.15",
3574
                 "phpstan/phpstan-strict-rules": "^1.5.2",
3574
                 "phpstan/phpstan-strict-rules": "^1.5.2",
3575
-                "phpunit/phpunit": "^10.5.3",
3576
-                "symfony/var-dumper": "^6.4.0"
3575
+                "phpunit/phpunit": "^10.5.9",
3576
+                "symfony/var-dumper": "^6.4.2"
3577
             },
3577
             },
3578
             "suggest": {
3578
             "suggest": {
3579
                 "ext-dom": "Required to use the XMLConverter and the HTMLConverter classes",
3579
                 "ext-dom": "Required to use the XMLConverter and the HTMLConverter classes",
3629
                     "type": "github"
3629
                     "type": "github"
3630
                 }
3630
                 }
3631
             ],
3631
             ],
3632
-            "time": "2023-12-29T07:34:53+00:00"
3632
+            "time": "2024-02-20T20:00:00+00:00"
3633
         },
3633
         },
3634
         {
3634
         {
3635
             "name": "league/flysystem",
3635
             "name": "league/flysystem",
4089
         },
4089
         },
4090
         {
4090
         {
4091
             "name": "livewire/livewire",
4091
             "name": "livewire/livewire",
4092
-            "version": "v3.4.4",
4092
+            "version": "v3.4.6",
4093
             "source": {
4093
             "source": {
4094
                 "type": "git",
4094
                 "type": "git",
4095
                 "url": "https://github.com/livewire/livewire.git",
4095
                 "url": "https://github.com/livewire/livewire.git",
4096
-                "reference": "c0489d4a76382f6dcf6e2702112f86aa089d0c8d"
4096
+                "reference": "7e7d638183b34fb61621455891869f5abfd55a82"
4097
             },
4097
             },
4098
             "dist": {
4098
             "dist": {
4099
                 "type": "zip",
4099
                 "type": "zip",
4100
-                "url": "https://api.github.com/repos/livewire/livewire/zipball/c0489d4a76382f6dcf6e2702112f86aa089d0c8d",
4101
-                "reference": "c0489d4a76382f6dcf6e2702112f86aa089d0c8d",
4100
+                "url": "https://api.github.com/repos/livewire/livewire/zipball/7e7d638183b34fb61621455891869f5abfd55a82",
4101
+                "reference": "7e7d638183b34fb61621455891869f5abfd55a82",
4102
                 "shasum": ""
4102
                 "shasum": ""
4103
             },
4103
             },
4104
             "require": {
4104
             "require": {
4152
             "description": "A front-end framework for Laravel.",
4152
             "description": "A front-end framework for Laravel.",
4153
             "support": {
4153
             "support": {
4154
                 "issues": "https://github.com/livewire/livewire/issues",
4154
                 "issues": "https://github.com/livewire/livewire/issues",
4155
-                "source": "https://github.com/livewire/livewire/tree/v3.4.4"
4155
+                "source": "https://github.com/livewire/livewire/tree/v3.4.6"
4156
             },
4156
             },
4157
             "funding": [
4157
             "funding": [
4158
                 {
4158
                 {
4160
                     "type": "github"
4160
                     "type": "github"
4161
                 }
4161
                 }
4162
             ],
4162
             ],
4163
-            "time": "2024-01-28T19:07:11+00:00"
4163
+            "time": "2024-02-20T14:04:25+00:00"
4164
         },
4164
         },
4165
         {
4165
         {
4166
             "name": "masterminds/html5",
4166
             "name": "masterminds/html5",
4772
         },
4772
         },
4773
         {
4773
         {
4774
             "name": "nikic/php-parser",
4774
             "name": "nikic/php-parser",
4775
-            "version": "v5.0.0",
4775
+            "version": "v5.0.1",
4776
             "source": {
4776
             "source": {
4777
                 "type": "git",
4777
                 "type": "git",
4778
                 "url": "https://github.com/nikic/PHP-Parser.git",
4778
                 "url": "https://github.com/nikic/PHP-Parser.git",
4779
-                "reference": "4a21235f7e56e713259a6f76bf4b5ea08502b9dc"
4779
+                "reference": "2218c2252c874a4624ab2f613d86ac32d227bc69"
4780
             },
4780
             },
4781
             "dist": {
4781
             "dist": {
4782
                 "type": "zip",
4782
                 "type": "zip",
4783
-                "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4a21235f7e56e713259a6f76bf4b5ea08502b9dc",
4784
-                "reference": "4a21235f7e56e713259a6f76bf4b5ea08502b9dc",
4783
+                "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/2218c2252c874a4624ab2f613d86ac32d227bc69",
4784
+                "reference": "2218c2252c874a4624ab2f613d86ac32d227bc69",
4785
                 "shasum": ""
4785
                 "shasum": ""
4786
             },
4786
             },
4787
             "require": {
4787
             "require": {
4824
             ],
4824
             ],
4825
             "support": {
4825
             "support": {
4826
                 "issues": "https://github.com/nikic/PHP-Parser/issues",
4826
                 "issues": "https://github.com/nikic/PHP-Parser/issues",
4827
-                "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.0"
4827
+                "source": "https://github.com/nikic/PHP-Parser/tree/v5.0.1"
4828
             },
4828
             },
4829
-            "time": "2024-01-07T17:17:35+00:00"
4829
+            "time": "2024-02-21T19:24:10+00:00"
4830
         },
4830
         },
4831
         {
4831
         {
4832
             "name": "nunomaduro/termwind",
4832
             "name": "nunomaduro/termwind",
6179
         },
6179
         },
6180
         {
6180
         {
6181
             "name": "spatie/laravel-tags",
6181
             "name": "spatie/laravel-tags",
6182
-            "version": "4.5.2",
6182
+            "version": "4.6.0",
6183
             "source": {
6183
             "source": {
6184
                 "type": "git",
6184
                 "type": "git",
6185
                 "url": "https://github.com/spatie/laravel-tags.git",
6185
                 "url": "https://github.com/spatie/laravel-tags.git",
6186
-                "reference": "627587878731650c52d79e2132cfd1410a18d428"
6186
+                "reference": "ddcc82e1149c4720c0eaa02d171f50a1cdd1cb46"
6187
             },
6187
             },
6188
             "dist": {
6188
             "dist": {
6189
                 "type": "zip",
6189
                 "type": "zip",
6190
-                "url": "https://api.github.com/repos/spatie/laravel-tags/zipball/627587878731650c52d79e2132cfd1410a18d428",
6191
-                "reference": "627587878731650c52d79e2132cfd1410a18d428",
6190
+                "url": "https://api.github.com/repos/spatie/laravel-tags/zipball/ddcc82e1149c4720c0eaa02d171f50a1cdd1cb46",
6191
+                "reference": "ddcc82e1149c4720c0eaa02d171f50a1cdd1cb46",
6192
                 "shasum": ""
6192
                 "shasum": ""
6193
             },
6193
             },
6194
             "require": {
6194
             "require": {
6195
-                "laravel/framework": "^8.67|^9.0|^10.0",
6195
+                "laravel/framework": "^8.67|^9.0|^10.0|^11.0",
6196
                 "nesbot/carbon": "^2.63",
6196
                 "nesbot/carbon": "^2.63",
6197
                 "php": "^8.0",
6197
                 "php": "^8.0",
6198
                 "spatie/eloquent-sortable": "^3.10|^4.0",
6198
                 "spatie/eloquent-sortable": "^3.10|^4.0",
6200
                 "spatie/laravel-translatable": "^4.6|^5.0|^6.0"
6200
                 "spatie/laravel-translatable": "^4.6|^5.0|^6.0"
6201
             },
6201
             },
6202
             "require-dev": {
6202
             "require-dev": {
6203
-                "orchestra/testbench": "^6.13|^7.0|^8.0",
6204
-                "pestphp/pest": "^1.22",
6203
+                "orchestra/testbench": "^6.13|^7.0|^8.0|^9.0",
6204
+                "pestphp/pest": "^1.22|^2.0",
6205
                 "phpunit/phpunit": "^9.5.2"
6205
                 "phpunit/phpunit": "^9.5.2"
6206
             },
6206
             },
6207
             "type": "library",
6207
             "type": "library",
6237
             ],
6237
             ],
6238
             "support": {
6238
             "support": {
6239
                 "issues": "https://github.com/spatie/laravel-tags/issues",
6239
                 "issues": "https://github.com/spatie/laravel-tags/issues",
6240
-                "source": "https://github.com/spatie/laravel-tags/tree/4.5.2"
6240
+                "source": "https://github.com/spatie/laravel-tags/tree/4.6.0"
6241
             },
6241
             },
6242
             "funding": [
6242
             "funding": [
6243
                 {
6243
                 {
6245
                     "type": "github"
6245
                     "type": "github"
6246
                 }
6246
                 }
6247
             ],
6247
             ],
6248
-            "time": "2024-01-19T22:10:13+00:00"
6248
+            "time": "2024-02-21T12:13:18+00:00"
6249
         },
6249
         },
6250
         {
6250
         {
6251
             "name": "spatie/laravel-translatable",
6251
             "name": "spatie/laravel-translatable",
6252
-            "version": "6.5.5",
6252
+            "version": "6.6.0",
6253
             "source": {
6253
             "source": {
6254
                 "type": "git",
6254
                 "type": "git",
6255
                 "url": "https://github.com/spatie/laravel-translatable.git",
6255
                 "url": "https://github.com/spatie/laravel-translatable.git",
6256
-                "reference": "d6dc7c9fe3c678ce50b2d6a4a7434fcbcfc3df4c"
6256
+                "reference": "11f0b548dd43b846a5bdca1431de173ac77ed349"
6257
             },
6257
             },
6258
             "dist": {
6258
             "dist": {
6259
                 "type": "zip",
6259
                 "type": "zip",
6260
-                "url": "https://api.github.com/repos/spatie/laravel-translatable/zipball/d6dc7c9fe3c678ce50b2d6a4a7434fcbcfc3df4c",
6261
-                "reference": "d6dc7c9fe3c678ce50b2d6a4a7434fcbcfc3df4c",
6260
+                "url": "https://api.github.com/repos/spatie/laravel-translatable/zipball/11f0b548dd43b846a5bdca1431de173ac77ed349",
6261
+                "reference": "11f0b548dd43b846a5bdca1431de173ac77ed349",
6262
                 "shasum": ""
6262
                 "shasum": ""
6263
             },
6263
             },
6264
             "require": {
6264
             "require": {
6265
-                "illuminate/database": "^9.0|^10.0",
6266
-                "illuminate/support": "^9.0|^10.0",
6265
+                "illuminate/database": "^9.0|^10.0|^11.0",
6266
+                "illuminate/support": "^9.0|^10.0|^11.0",
6267
                 "php": "^8.0",
6267
                 "php": "^8.0",
6268
                 "spatie/laravel-package-tools": "^1.11"
6268
                 "spatie/laravel-package-tools": "^1.11"
6269
             },
6269
             },
6270
             "require-dev": {
6270
             "require-dev": {
6271
                 "mockery/mockery": "^1.4",
6271
                 "mockery/mockery": "^1.4",
6272
-                "orchestra/testbench": "^7.0|^8.0",
6273
-                "pestphp/pest": "^1.20"
6272
+                "orchestra/testbench": "^7.0|^8.0|^9.0",
6273
+                "pestphp/pest": "^1.20|^2.0"
6274
             },
6274
             },
6275
             "type": "library",
6275
             "type": "library",
6276
             "extra": {
6276
             "extra": {
6319
             ],
6319
             ],
6320
             "support": {
6320
             "support": {
6321
                 "issues": "https://github.com/spatie/laravel-translatable/issues",
6321
                 "issues": "https://github.com/spatie/laravel-translatable/issues",
6322
-                "source": "https://github.com/spatie/laravel-translatable/tree/6.5.5"
6322
+                "source": "https://github.com/spatie/laravel-translatable/tree/6.6.0"
6323
             },
6323
             },
6324
             "funding": [
6324
             "funding": [
6325
                 {
6325
                 {
6327
                     "type": "github"
6327
                     "type": "github"
6328
                 }
6328
                 }
6329
             ],
6329
             ],
6330
-            "time": "2023-12-06T10:56:22+00:00"
6330
+            "time": "2024-02-23T13:52:34+00:00"
6331
         },
6331
         },
6332
         {
6332
         {
6333
             "name": "squirephp/model",
6333
             "name": "squirephp/model",
9277
         },
9277
         },
9278
         {
9278
         {
9279
             "name": "laravel/pint",
9279
             "name": "laravel/pint",
9280
-            "version": "v1.13.11",
9280
+            "version": "v1.14.0",
9281
             "source": {
9281
             "source": {
9282
                 "type": "git",
9282
                 "type": "git",
9283
                 "url": "https://github.com/laravel/pint.git",
9283
                 "url": "https://github.com/laravel/pint.git",
9284
-                "reference": "60a163c3e7e3346a1dec96d3e6f02e6465452552"
9284
+                "reference": "6b127276e3f263f7bb17d5077e9e0269e61b2a0e"
9285
             },
9285
             },
9286
             "dist": {
9286
             "dist": {
9287
                 "type": "zip",
9287
                 "type": "zip",
9288
-                "url": "https://api.github.com/repos/laravel/pint/zipball/60a163c3e7e3346a1dec96d3e6f02e6465452552",
9289
-                "reference": "60a163c3e7e3346a1dec96d3e6f02e6465452552",
9288
+                "url": "https://api.github.com/repos/laravel/pint/zipball/6b127276e3f263f7bb17d5077e9e0269e61b2a0e",
9289
+                "reference": "6b127276e3f263f7bb17d5077e9e0269e61b2a0e",
9290
                 "shasum": ""
9290
                 "shasum": ""
9291
             },
9291
             },
9292
             "require": {
9292
             "require": {
9339
                 "issues": "https://github.com/laravel/pint/issues",
9339
                 "issues": "https://github.com/laravel/pint/issues",
9340
                 "source": "https://github.com/laravel/pint"
9340
                 "source": "https://github.com/laravel/pint"
9341
             },
9341
             },
9342
-            "time": "2024-02-13T17:20:13+00:00"
9342
+            "time": "2024-02-20T17:38:05+00:00"
9343
         },
9343
         },
9344
         {
9344
         {
9345
             "name": "laravel/sail",
9345
             "name": "laravel/sail",
9346
-            "version": "v1.27.4",
9346
+            "version": "v1.28.0",
9347
             "source": {
9347
             "source": {
9348
                 "type": "git",
9348
                 "type": "git",
9349
                 "url": "https://github.com/laravel/sail.git",
9349
                 "url": "https://github.com/laravel/sail.git",
9350
-                "reference": "3047e1a157fad968cc5f6e620d5cbe5c0867fffd"
9350
+                "reference": "a05861ca9b04558b1ec1f36cff521a271a259b6c"
9351
             },
9351
             },
9352
             "dist": {
9352
             "dist": {
9353
                 "type": "zip",
9353
                 "type": "zip",
9354
-                "url": "https://api.github.com/repos/laravel/sail/zipball/3047e1a157fad968cc5f6e620d5cbe5c0867fffd",
9355
-                "reference": "3047e1a157fad968cc5f6e620d5cbe5c0867fffd",
9354
+                "url": "https://api.github.com/repos/laravel/sail/zipball/a05861ca9b04558b1ec1f36cff521a271a259b6c",
9355
+                "reference": "a05861ca9b04558b1ec1f36cff521a271a259b6c",
9356
                 "shasum": ""
9356
                 "shasum": ""
9357
             },
9357
             },
9358
             "require": {
9358
             "require": {
9401
                 "issues": "https://github.com/laravel/sail/issues",
9401
                 "issues": "https://github.com/laravel/sail/issues",
9402
                 "source": "https://github.com/laravel/sail"
9402
                 "source": "https://github.com/laravel/sail"
9403
             },
9403
             },
9404
-            "time": "2024-02-08T15:24:21+00:00"
9404
+            "time": "2024-02-20T15:11:00+00:00"
9405
         },
9405
         },
9406
         {
9406
         {
9407
             "name": "mockery/mockery",
9407
             "name": "mockery/mockery",

+ 2
- 0
database/migrations/2023_09_03_100000_create_accounting_tables.php 查看文件

84
             $table->text('access_token')->nullable();
84
             $table->text('access_token')->nullable();
85
             $table->string('identifier')->unique()->nullable(); // Plaid
85
             $table->string('identifier')->unique()->nullable(); // Plaid
86
             $table->string('item_id')->nullable();
86
             $table->string('item_id')->nullable();
87
+            $table->string('currency_code')->nullable();
88
+            $table->double('current_balance')->default(0);
87
             $table->string('name');
89
             $table->string('name');
88
             $table->string('mask');
90
             $table->string('mask');
89
             $table->string('type')->default(BankAccountType::DEFAULT);
91
             $table->string('type')->default(BankAccountType::DEFAULT);

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

397
             }
397
             }
398
         },
398
         },
399
         "node_modules/@jridgewell/gen-mapping": {
399
         "node_modules/@jridgewell/gen-mapping": {
400
-            "version": "0.3.3",
401
-            "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
402
-            "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
400
+            "version": "0.3.4",
401
+            "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.4.tgz",
402
+            "integrity": "sha512-Oud2QPM5dHviZNn4y/WhhYKSXksv+1xLEIsNrAbGcFzUN3ubqWRFT5gwPchNc5NuzILOU4tPBDTZ4VwhL8Y7cw==",
403
             "dev": true,
403
             "dev": true,
404
             "dependencies": {
404
             "dependencies": {
405
                 "@jridgewell/set-array": "^1.0.1",
405
                 "@jridgewell/set-array": "^1.0.1",
435
             "dev": true
435
             "dev": true
436
         },
436
         },
437
         "node_modules/@jridgewell/trace-mapping": {
437
         "node_modules/@jridgewell/trace-mapping": {
438
-            "version": "0.3.22",
439
-            "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz",
440
-            "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==",
438
+            "version": "0.3.23",
439
+            "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.23.tgz",
440
+            "integrity": "sha512-9/4foRoUKp8s96tSkh8DlAAc5A0Ty8vLXld+l9gjKKY6ckwI8G15f0hskGmuLZu78ZlGa1vtsfOa+lnB4vG6Jg==",
441
             "dev": true,
441
             "dev": true,
442
             "dependencies": {
442
             "dependencies": {
443
                 "@jridgewell/resolve-uri": "^3.1.0",
443
                 "@jridgewell/resolve-uri": "^3.1.0",
697
             }
697
             }
698
         },
698
         },
699
         "node_modules/caniuse-lite": {
699
         "node_modules/caniuse-lite": {
700
-            "version": "1.0.30001588",
701
-            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001588.tgz",
702
-            "integrity": "sha512-+hVY9jE44uKLkH0SrUTqxjxqNTOWHsbnQDIKjwkZ3lNTzUUVdBLBGXtj/q5Mp5u98r3droaZAewQuEDzjQdZlQ==",
700
+            "version": "1.0.30001589",
701
+            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001589.tgz",
702
+            "integrity": "sha512-vNQWS6kI+q6sBlHbh71IIeC+sRwK2N3EDySc/updIGhIee2x5z00J4c1242/5/d6EpEMdOnk/m+6tuk4/tcsqg==",
703
             "dev": true,
703
             "dev": true,
704
             "funding": [
704
             "funding": [
705
                 {
705
                 {
845
             "dev": true
845
             "dev": true
846
         },
846
         },
847
         "node_modules/electron-to-chromium": {
847
         "node_modules/electron-to-chromium": {
848
-            "version": "1.4.673",
849
-            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.673.tgz",
850
-            "integrity": "sha512-zjqzx4N7xGdl5468G+vcgzDhaHkaYgVcf9MqgexcTqsl2UHSCmOj/Bi3HAprg4BZCpC7HyD8a6nZl6QAZf72gw==",
848
+            "version": "1.4.681",
849
+            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.681.tgz",
850
+            "integrity": "sha512-1PpuqJUFWoXZ1E54m8bsLPVYwIVCRzvaL+n5cjigGga4z854abDnFRc+cTa2th4S79kyGqya/1xoR7h+Y5G5lg==",
851
             "dev": true
851
             "dev": true
852
         },
852
         },
853
         "node_modules/emoji-regex": {
853
         "node_modules/emoji-regex": {

+ 1
- 1
resources/data/lang/it.json 查看文件

241
     "Manual": "Manuale",
241
     "Manual": "Manuale",
242
     "Accounts": "Account",
242
     "Accounts": "Account",
243
     "Credit": "Credito"
243
     "Credit": "Credito"
244
-}
244
+}

+ 3
- 0
resources/views/components/icons/logo.blade.php 查看文件

1
+<svg viewBox="0 0 370.25 66.915" fill="currentColor" class="h-5/6 text-gray-700 dark:text-gray-200">
2
+    <path d="M41.035 51.387q1.164 0 1.94.815t.775 1.978v5.663q0 1.086-.776 1.9t-1.939.815H2.715q-1.086 0-1.9-.815T0 59.843V54.18q0-1.164.814-1.978t1.901-.815h38.32zM2.715 39.364q-1.086 0-1.9-.815T0 36.65V30.83q0-1.086.814-1.9t1.901-.815h34.83q1.163 0 1.977.815t.815 1.9v5.818q0 1.086-.815 1.9t-1.978.815H2.715zM41.035 5q1.164 0 1.94.814t.775 1.901v5.663q0 1.163-.776 1.978t-1.939.814H2.715q-1.086 0-1.9-.814T0 13.378V7.715q0-1.086.814-1.9T2.715 5h38.32zM93.125 60.807 78.772 36.53h-3.32v24.277H64V5.65h20.002c12.482 0 17.75 7.368 17.75 16.49 0 7.406-4.2 12.215-11.681 13.819l16.604 24.85h-13.55zM75.452 15.152v13.17h6.756c6.108 0 8.589-2.596 8.589-6.566 0-3.932-2.481-6.604-8.589-6.604h-6.756zm62.563-9.502c10.802 0 17.52 8.016 17.52 17.826 0 10.04-6.718 17.56-17.52 17.56h-11.49v19.772h-11.452V5.65h22.941zm-1.909 25.803c5.726 0 8.398-3.168 8.398-8.13 0-4.772-2.672-8.093-8.398-8.093h-9.581v16.223h9.581zm69.472 13.513c0 11.031-8.665 15.994-18.78 16.566v5.383h-4.237v-5.383c-10.574-.649-18.78-6.26-20.346-15.917l11.833-2.749c.611 5.268 3.97 8.322 8.512 9.162V37.179c-.572-.19-1.145-.344-1.717-.534-8.093-2.482-16.72-5.497-16.72-15.612 0-10.001 7.864-15.727 18.438-16.109V0h4.237v5.077c8.55.801 16.452 5 18.436 14.352l-11.031 2.749c-.84-4.543-3.55-6.986-7.405-7.673v13.551c.19.076.42.114.61.19 8.283 2.482 18.17 5.765 18.17 16.72zm-29.888-24.2c0 3.053 2.901 4.656 6.87 5.992V14.353c-3.855.458-6.87 2.405-6.87 6.412zm11.108 31.414c4.046-.42 6.985-2.481 6.947-6.604-.038-3.588-2.94-5.497-6.947-6.985v13.59zm67.603 8.627-4.008-11.376h-23.17l-4.008 11.376h-11.718L231.804 5.65h14.009l20.307 55.157h-11.718zm-23.82-20.956h16.451l-8.206-23.4zm80.734 20.956-4.008-11.376h-23.17l-4.008 11.376H268.41L288.718 5.65h14.008l20.308 55.157h-11.719zm-23.82-20.956h16.451l-8.207-23.4zm62.142 21.72c-11.833 0-21.07-5.688-22.75-16.033l11.833-2.71c.763 6.183 5.306 9.39 11.375 9.39 4.657 0 8.36-2.061 8.322-6.719-.039-5.19-6.146-6.832-12.864-8.894-8.092-2.519-16.757-5.496-16.757-15.65 0-10.268 8.397-16.07 19.467-16.07 9.696 0 19.468 3.932 21.682 14.505l-11.07 2.749c-1.03-5.497-4.733-7.902-9.925-7.902-4.618 0-8.55 1.909-8.55 6.451 0 4.237 5.42 5.65 11.68 7.52 8.322 2.519 18.17 5.725 18.17 16.719 0 11.718-9.81 16.642-20.612 16.642z" />
3
+</svg>

+ 0
- 3
routes/api.php 查看文件

1
 <?php
1
 <?php
2
 
2
 
3
-use App\Http\Controllers\PlaidController;
4
 use Illuminate\Http\Request;
3
 use Illuminate\Http\Request;
5
 use Illuminate\Support\Facades\Route;
4
 use Illuminate\Support\Facades\Route;
6
 
5
 
18
 Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
17
 Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
19
     return $request->user();
18
     return $request->user();
20
 });
19
 });
21
-
22
-Route::get('/simulate-plaid', PlaidController::class);

Loading…
取消
儲存