Browse Source

update: Plaid automatic transaction import with double entry accounting

3.x
wallo 1 year ago
parent
commit
37d782edee

+ 1
- 1
app/Casts/MoneyCast.php View File

@@ -11,7 +11,7 @@ class MoneyCast implements CastsAttributes
11 11
 {
12 12
     public function get(Model $model, string $key, mixed $value, array $attributes): string
13 13
     {
14
-        $currency_code = $model->getAttribute('currency_code');
14
+        $currency_code = $model->getAttribute('currency_code') ?? CurrencyAccessor::getDefaultCurrency();
15 15
 
16 16
         if ($value !== null) {
17 17
             return money($value, $currency_code)->formatSimple();

+ 6
- 8
app/Filament/Company/Resources/Accounting/TransactionResource.php View File

@@ -5,7 +5,7 @@ namespace App\Filament\Company\Resources\Accounting;
5 5
 use App\Enums\DateFormat;
6 6
 use App\Filament\Company\Resources\Accounting\TransactionResource\Pages;
7 7
 use App\Models\Accounting\Transaction;
8
-use App\Models\Banking\Account;
8
+use App\Models\Banking\BankAccount;
9 9
 use App\Models\Setting\Localization;
10 10
 use Filament\Forms;
11 11
 use Filament\Forms\Form;
@@ -49,14 +49,12 @@ class TransactionResource extends Resource
49 49
                 Forms\Components\TextInput::make('amount')
50 50
                     ->label('Amount')
51 51
                     ->money(static function (Forms\Get $get) {
52
-                        $account = $get('account_id');
52
+                        $bankAccount = $get('bank_account_id');
53
+                        $bankAccount = BankAccount::find($bankAccount);
54
+                        $account = $bankAccount->account ?? null;
53 55
 
54 56
                         if ($account) {
55
-                            $account = Account::find($account);
56
-
57
-                            if ($account) {
58
-                                return $account->currency_code;
59
-                            }
57
+                            return $account->currency_code;
60 58
                         }
61 59
 
62 60
                         return 'USD';
@@ -98,7 +96,7 @@ class TransactionResource extends Resource
98 96
                     ->sortable()
99 97
                     ->weight(FontWeight::Medium)
100 98
                     ->color(static fn (Transaction $record) => $record->type === 'expense' ? 'danger' : null)
101
-                    ->currency(static fn (Transaction $record) => $record->account->currency_code, true),
99
+                    ->currency(static fn (Transaction $record) => $record->bankAccount->account->currency_code ?? 'USD', true),
102 100
             ])
103 101
             ->filters([
104 102
                 //

+ 1
- 1
app/Filament/Company/Resources/Banking/AccountResource.php View File

@@ -198,7 +198,7 @@ class AccountResource extends Resource
198 198
                     ->iconPosition('after')
199 199
                     ->description(static fn (BankAccount $record) => $record->mask ?: 'N/A')
200 200
                     ->sortable(),
201
-                Tables\Columns\TextColumn::make('account.starting_balance')
201
+                Tables\Columns\TextColumn::make('account.ending_balance')
202 202
                     ->localizeLabel('Current Balance')
203 203
                     ->sortable()
204 204
                     ->currency(static fn (BankAccount $record) => $record->account->currency_code, true),

+ 181
- 9
app/Listeners/HandleTransactionImport.php View File

@@ -2,15 +2,23 @@
2 2
 
3 3
 namespace App\Listeners;
4 4
 
5
+use App\Enums\Accounting\AccountCategory;
6
+use App\Enums\Accounting\AccountType;
5 7
 use App\Events\StartTransactionImport;
8
+use App\Models\Accounting\Account;
6 9
 use App\Models\Accounting\AccountSubtype;
10
+use App\Models\Accounting\Transaction;
7 11
 use App\Models\Banking\BankAccount;
8 12
 use App\Models\Banking\ConnectedBankAccount;
9 13
 use App\Models\Company;
14
+use App\Models\Setting\Category;
10 15
 use App\Models\Setting\Currency;
11 16
 use App\Services\PlaidService;
12 17
 use App\Utilities\Currency\CurrencyAccessor;
18
+use Illuminate\Support\Carbon;
13 19
 use Illuminate\Support\Facades\DB;
20
+use Illuminate\Support\Facades\Log;
21
+use Illuminate\Support\Str;
14 22
 
15 23
 class HandleTransactionImport
16 24
 {
@@ -43,20 +51,184 @@ class HandleTransactionImport
43 51
 
44 52
         $accessToken = $connectedBankAccount->access_token;
45 53
 
46
-        if ($selectedBankAccountId === 'new') {
47
-            $bankAccount = $this->processNewBankAccount($company, $connectedBankAccount, $accessToken);
48
-        } else {
49
-            $bankAccount = BankAccount::find($selectedBankAccountId);
54
+        $bankAccount = $selectedBankAccountId === 'new'
55
+            ? $this->processNewBankAccount($company, $connectedBankAccount, $accessToken)
56
+            : BankAccount::find($selectedBankAccountId);
57
+
58
+        if ($bankAccount) {
59
+            $connectedBankAccount->update([
60
+                'bank_account_id' => $bankAccount->id,
61
+                'import_transactions' => true,
62
+            ]);
63
+
64
+            $account = $bankAccount->account;
65
+
66
+            $this->processTransactions($startDate, $company, $connectedBankAccount, $accessToken, $account);
67
+        }
68
+    }
69
+
70
+    public function processTransactions($startDate, Company $company, ConnectedBankAccount $connectedBankAccount, $accessToken, Account $account): void
71
+    {
72
+        $endDate = Carbon::now()->toDateString();
73
+        $startDate = Carbon::parse($startDate)->toDateString();
74
+
75
+        $transactionsResponse = $this->plaid->getTransactions($accessToken, $startDate, $endDate, [
76
+            'account_ids' => [$connectedBankAccount->external_account_id],
77
+        ]);
50 78
 
51
-            if ($bankAccount === null) {
52
-                return;
79
+        if (! empty($transactionsResponse->transactions)) {
80
+            foreach ($transactionsResponse->transactions as $transaction) {
81
+                $this->storeTransaction($company, $account, $connectedBankAccount, $transaction);
53 82
             }
54 83
         }
84
+    }
85
+
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';
92
+        }
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;
55 102
 
56
-        $connectedBankAccount->update([
57
-            'bank_account_id' => $bankAccount->id,
58
-            'import_transactions' => true,
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,
59 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;
60 232
     }
61 233
 
62 234
     public function processNewBankAccount(Company $company, ConnectedBankAccount $connectedBankAccount, $accessToken): BankAccount

+ 4
- 4
app/Listeners/SyncTransactionsFromPlaid.php View File

@@ -4,7 +4,7 @@ namespace App\Listeners;
4 4
 
5 5
 use App\Enums\Accounting\AccountType;
6 6
 use App\Events\PlaidSuccess;
7
-use App\Models\Accounting\Chart;
7
+use App\Models\Accounting\Account;
8 8
 use App\Models\Company;
9 9
 use App\Models\Setting\Category;
10 10
 use App\Services\PlaidService;
@@ -141,12 +141,12 @@ class SyncTransactionsFromPlaid
141 141
         return $uncategorizedCategory;
142 142
     }
143 143
 
144
-    public function getChartFromTransaction(Company $company, $transaction, string $transactionType): Chart
144
+    public function getChartFromTransaction(Company $company, $transaction, string $transactionType): Account
145 145
     {
146 146
         if ($transactionType === 'income') {
147
-            $chart = $company->charts()->where('type', AccountType::OperatingRevenue)->where('name', 'Uncategorized Income')->first();
147
+            $chart = $company->accounts()->where('type', AccountType::OperatingRevenue)->where('name', 'Uncategorized Income')->first();
148 148
         } else {
149
-            $chart = $company->charts()->where('type', AccountType::OperatingExpense)->where('name', 'Uncategorized Expense')->first();
149
+            $chart = $company->accounts()->where('type', AccountType::OperatingExpense)->where('name', 'Uncategorized Expense')->first();
150 150
         }
151 151
 
152 152
         return $chart;

+ 5
- 0
app/Models/Accounting/Account.php View File

@@ -3,6 +3,8 @@
3 3
 namespace App\Models\Accounting;
4 4
 
5 5
 use App\Casts\MoneyCast;
6
+use App\Enums\Accounting\AccountCategory;
7
+use App\Enums\Accounting\AccountType;
6 8
 use App\Models\Setting\Category;
7 9
 use App\Models\Setting\Currency;
8 10
 use App\Traits\Blamable;
@@ -37,6 +39,7 @@ class Account extends Model
37 39
         'starting_balance',
38 40
         'debit_balance',
39 41
         'credit_balance',
42
+        'net_movement',
40 43
         'ending_balance',
41 44
         'description',
42 45
         'active',
@@ -48,6 +51,8 @@ class Account extends Model
48 51
     ];
49 52
 
50 53
     protected $casts = [
54
+        'category' => AccountCategory::class,
55
+        'type' => AccountType::class,
51 56
         'starting_balance' => MoneyCast::class,
52 57
         'debit_balance' => MoneyCast::class,
53 58
         'credit_balance' => MoneyCast::class,

+ 6
- 0
app/Models/Accounting/JournalEntry.php View File

@@ -3,6 +3,7 @@
3 3
 namespace App\Models\Accounting;
4 4
 
5 5
 use App\Casts\MoneyCast;
6
+use App\Models\Banking\BankAccount;
6 7
 use App\Traits\Blamable;
7 8
 use App\Traits\CompanyOwned;
8 9
 use Database\Factories\Accounting\JournalEntryFactory;
@@ -58,6 +59,11 @@ class JournalEntry extends Model
58 59
         return $query->where('type', 'credit');
59 60
     }
60 61
 
62
+    public function bankAccount(): BelongsTo
63
+    {
64
+        return $this->account()->where('accountable_type', BankAccount::class);
65
+    }
66
+
61 67
     public function createdBy(): BelongsTo
62 68
     {
63 69
         return $this->belongsTo(FilamentCompanies::userModel(), 'created_by');

+ 7
- 0
app/Models/Accounting/Transaction.php View File

@@ -3,6 +3,7 @@
3 3
 namespace App\Models\Accounting;
4 4
 
5 5
 use App\Casts\MoneyCast;
6
+use App\Models\Banking\BankAccount;
6 7
 use App\Models\Common\Contact;
7 8
 use App\Models\Setting\Category;
8 9
 use App\Traits\Blamable;
@@ -24,6 +25,7 @@ class Transaction extends Model
24 25
     protected $fillable = [
25 26
         'company_id',
26 27
         'category_id',
28
+        'bank_account_id', // 'account_id' => 'bank_account_id'
27 29
         'contact_id',
28 30
         'type',
29 31
         'method',
@@ -66,6 +68,11 @@ class Transaction extends Model
66 68
         return $this->hasMany(JournalEntry::class, 'transaction_id');
67 69
     }
68 70
 
71
+    public function bankAccount(): BelongsTo
72
+    {
73
+        return $this->belongsTo(BankAccount::class, 'bank_account_id');
74
+    }
75
+
69 76
     public function createdBy(): BelongsTo
70 77
     {
71 78
         return $this->belongsTo(FilamentCompanies::userModel(), 'created_by');

+ 106
- 0
app/Observers/JournalEntryObserver.php View File

@@ -0,0 +1,106 @@
1
+<?php
2
+
3
+namespace App\Observers;
4
+
5
+use App\Enums\Accounting\AccountCategory;
6
+use App\Models\Accounting\Account;
7
+use App\Models\Accounting\JournalEntry;
8
+
9
+class JournalEntryObserver
10
+{
11
+    /**
12
+     * Handle the JournalEntry "created" event.
13
+     */
14
+    public function created(JournalEntry $journalEntry): void
15
+    {
16
+        $account = $journalEntry->account;
17
+
18
+        if ($account) {
19
+            $this->adjustBalance($account, $journalEntry->type, $journalEntry->amount);
20
+        }
21
+    }
22
+
23
+    private function updateEndingBalance(Account $account): void
24
+    {
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
+        };
29
+
30
+        $account->net_movement = $netMovementStrategy;
31
+
32
+        if (in_array($account->category, [AccountCategory::Asset, AccountCategory::Liability, AccountCategory::Equity], true)) {
33
+            $account->ending_balance = $account->starting_balance + $account->net_movement;
34
+        }
35
+
36
+        $account->save();
37
+    }
38
+
39
+    /**
40
+     * Handle the JournalEntry "updated" event.
41
+     */
42
+    public function updated(JournalEntry $journalEntry): void
43
+    {
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');
51
+
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
+        }
60
+
61
+        $newAccount = ($accountChanged) ? Account::find($journalEntry->account_id) : $journalEntry->account;
62
+
63
+        if ($newAccount) {
64
+            $this->adjustBalance($newAccount, $journalEntry->type, $journalEntry->amount);
65
+        }
66
+    }
67
+
68
+    private function adjustBalance(Account $account, $type, $amount): void
69
+    {
70
+        if ($type === 'debit') {
71
+            $account->debit_balance += $amount;
72
+        } elseif ($type === 'credit') {
73
+            $account->credit_balance += $amount;
74
+        }
75
+
76
+        $this->updateEndingBalance($account);
77
+    }
78
+
79
+    /**
80
+     * Handle the JournalEntry "deleted" event.
81
+     */
82
+    public function deleted(JournalEntry $journalEntry): void
83
+    {
84
+        $account = $journalEntry->account;
85
+
86
+        if ($account) {
87
+            $this->adjustBalance($account, $journalEntry->type, -$journalEntry->amount);
88
+        }
89
+    }
90
+
91
+    /**
92
+     * Handle the JournalEntry "restored" event.
93
+     */
94
+    public function restored(JournalEntry $journalEntry): void
95
+    {
96
+        //
97
+    }
98
+
99
+    /**
100
+     * Handle the JournalEntry "force deleted" event.
101
+     */
102
+    public function forceDeleted(JournalEntry $journalEntry): void
103
+    {
104
+        //
105
+    }
106
+}

+ 3
- 0
app/Providers/EventServiceProvider.php View File

@@ -23,11 +23,13 @@ use App\Listeners\SyncWithCompanyDefaults;
23 23
 use App\Listeners\UpdateAccountBalances;
24 24
 use App\Listeners\UpdateCurrencyRates;
25 25
 use App\Models\Accounting\Account;
26
+use App\Models\Accounting\JournalEntry;
26 27
 use App\Models\Banking\BankAccount;
27 28
 use App\Models\Setting\Currency;
28 29
 use App\Observers\AccountObserver;
29 30
 use App\Observers\BankAccountObserver;
30 31
 use App\Observers\CurrencyObserver;
32
+use App\Observers\JournalEntryObserver;
31 33
 use Illuminate\Auth\Events\Registered;
32 34
 use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
33 35
 use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
@@ -83,6 +85,7 @@ class EventServiceProvider extends ServiceProvider
83 85
         Currency::class => [CurrencyObserver::class],
84 86
         BankAccount::class => [BankAccountObserver::class],
85 87
         Account::class => [AccountObserver::class],
88
+        JournalEntry::class => [JournalEntryObserver::class],
86 89
     ];
87 90
 
88 91
     /**

+ 12
- 0
app/Services/PlaidService.php View File

@@ -290,4 +290,16 @@ class PlaidService
290 290
 
291 291
         return $this->sendRequest('transactions/sync', $data);
292 292
     }
293
+
294
+    public function getTransactions(string $access_token, string $start_date, string $end_date, array $options = []): object
295
+    {
296
+        $data = [
297
+            'access_token' => $access_token,
298
+            'start_date' => $start_date,
299
+            'end_date' => $end_date,
300
+            'options' => (object) $options,
301
+        ];
302
+
303
+        return $this->sendRequest('transactions/get', $data);
304
+    }
293 305
 }

+ 6
- 6
composer.lock View File

@@ -1461,16 +1461,16 @@
1461 1461
         },
1462 1462
         {
1463 1463
             "name": "doctrine/inflector",
1464
-            "version": "2.0.9",
1464
+            "version": "2.0.10",
1465 1465
             "source": {
1466 1466
                 "type": "git",
1467 1467
                 "url": "https://github.com/doctrine/inflector.git",
1468
-                "reference": "2930cd5ef353871c821d5c43ed030d39ac8cfe65"
1468
+                "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc"
1469 1469
             },
1470 1470
             "dist": {
1471 1471
                 "type": "zip",
1472
-                "url": "https://api.github.com/repos/doctrine/inflector/zipball/2930cd5ef353871c821d5c43ed030d39ac8cfe65",
1473
-                "reference": "2930cd5ef353871c821d5c43ed030d39ac8cfe65",
1472
+                "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc",
1473
+                "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc",
1474 1474
                 "shasum": ""
1475 1475
             },
1476 1476
             "require": {
@@ -1532,7 +1532,7 @@
1532 1532
             ],
1533 1533
             "support": {
1534 1534
                 "issues": "https://github.com/doctrine/inflector/issues",
1535
-                "source": "https://github.com/doctrine/inflector/tree/2.0.9"
1535
+                "source": "https://github.com/doctrine/inflector/tree/2.0.10"
1536 1536
             },
1537 1537
             "funding": [
1538 1538
                 {
@@ -1548,7 +1548,7 @@
1548 1548
                     "type": "tidelift"
1549 1549
                 }
1550 1550
             ],
1551
-            "time": "2024-01-15T18:05:13+00:00"
1551
+            "time": "2024-02-18T20:23:39+00:00"
1552 1552
         },
1553 1553
         {
1554 1554
             "name": "doctrine/lexer",

+ 1
- 0
database/migrations/2023_09_03_100000_create_accounting_tables.php View File

@@ -49,6 +49,7 @@ return new class extends Migration
49 49
             $table->bigInteger('starting_balance')->default(0);
50 50
             $table->bigInteger('debit_balance')->default(0);
51 51
             $table->bigInteger('credit_balance')->default(0);
52
+            $table->bigInteger('net_movement')->default(0);
52 53
             $table->bigInteger('ending_balance')->default(0);
53 54
             $table->text('description')->nullable();
54 55
             $table->boolean('active')->default(true);

+ 1
- 0
database/migrations/2024_01_01_234943_create_transactions_table.php View File

@@ -15,6 +15,7 @@ return new class extends Migration
15 15
             $table->id();
16 16
             $table->foreignId('company_id')->constrained()->cascadeOnDelete();
17 17
             $table->foreignId('category_id')->nullable()->constrained()->nullOnDelete();
18
+            $table->foreignId('bank_account_id')->nullable()->constrained()->nullOnDelete();
18 19
             $table->foreignId('contact_id')->nullable()->constrained()->nullOnDelete();
19 20
             $table->string('type'); // income, expense, other
20 21
             $table->string('method'); // deposit, withdrawal

+ 6
- 6
package-lock.json View File

@@ -697,9 +697,9 @@
697 697
             }
698 698
         },
699 699
         "node_modules/caniuse-lite": {
700
-            "version": "1.0.30001587",
701
-            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001587.tgz",
702
-            "integrity": "sha512-HMFNotUmLXn71BQxg8cijvqxnIAofforZOwGsxyXJ0qugTdspUF4sPSJ2vhgprHCB996tIDzEq1ubumPDV8ULA==",
700
+            "version": "1.0.30001588",
701
+            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001588.tgz",
702
+            "integrity": "sha512-+hVY9jE44uKLkH0SrUTqxjxqNTOWHsbnQDIKjwkZ3lNTzUUVdBLBGXtj/q5Mp5u98r3droaZAewQuEDzjQdZlQ==",
703 703
             "dev": true,
704 704
             "funding": [
705 705
                 {
@@ -1551,9 +1551,9 @@
1551 1551
             }
1552 1552
         },
1553 1553
         "node_modules/postcss-load-config/node_modules/lilconfig": {
1554
-            "version": "3.1.0",
1555
-            "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.0.tgz",
1556
-            "integrity": "sha512-p3cz0JV5vw/XeouBU3Ldnp+ZkBjE+n8ydJ4mcwBrOiXXPqNlrzGBqWs9X4MWF7f+iKUBu794Y8Hh8yawiJbCjw==",
1554
+            "version": "3.1.1",
1555
+            "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz",
1556
+            "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==",
1557 1557
             "dev": true,
1558 1558
             "engines": {
1559 1559
                 "node": ">=14"

Loading…
Cancel
Save