Browse Source

wip testing

3.x
Andrew Wallo 1 year ago
parent
commit
3ee921e8ee

+ 9
- 19
app/Collections/Accounting/JournalEntryCollection.php View File

2
 
2
 
3
 namespace App\Collections\Accounting;
3
 namespace App\Collections\Accounting;
4
 
4
 
5
+use App\Enums\Accounting\JournalEntryType;
5
 use App\Models\Accounting\JournalEntry;
6
 use App\Models\Accounting\JournalEntry;
6
 use App\Utilities\Currency\CurrencyAccessor;
7
 use App\Utilities\Currency\CurrencyAccessor;
7
-use App\Utilities\Currency\CurrencyConverter;
8
 use App\ValueObjects\Money;
8
 use App\ValueObjects\Money;
9
 use Illuminate\Database\Eloquent\Collection;
9
 use Illuminate\Database\Eloquent\Collection;
10
 
10
 
12
 {
12
 {
13
     public function sumDebits(): Money
13
     public function sumDebits(): Money
14
     {
14
     {
15
-        $total = $this->reduce(static function ($carry, JournalEntry $item) {
16
-            if ($item->type->isDebit()) {
17
-                $amountAsInteger = CurrencyConverter::prepareForAccessor($item->amount, CurrencyAccessor::getDefaultCurrency());
18
-
19
-                return bcadd($carry, $amountAsInteger, 0);
20
-            }
21
-
22
-            return $carry;
23
-        }, 0);
15
+        $total = $this->where('type', JournalEntryType::Debit)
16
+            ->sum(static function (JournalEntry $item) {
17
+                return $item->rawValue('amount');
18
+            });
24
 
19
 
25
         return new Money($total, CurrencyAccessor::getDefaultCurrency());
20
         return new Money($total, CurrencyAccessor::getDefaultCurrency());
26
     }
21
     }
27
 
22
 
28
     public function sumCredits(): Money
23
     public function sumCredits(): Money
29
     {
24
     {
30
-        $total = $this->reduce(static function ($carry, JournalEntry $item) {
31
-            if ($item->type->isCredit()) {
32
-                $amountAsInteger = CurrencyConverter::prepareForAccessor($item->amount, CurrencyAccessor::getDefaultCurrency());
33
-
34
-                return bcadd($carry, $amountAsInteger, 0);
35
-            }
36
-
37
-            return $carry;
38
-        }, 0);
25
+        $total = $this->where('type', JournalEntryType::Credit)
26
+            ->sum(static function (JournalEntry $item) {
27
+                return $item->rawValue('amount');
28
+            });
39
 
29
 
40
         return new Money($total, CurrencyAccessor::getDefaultCurrency());
30
         return new Money($total, CurrencyAccessor::getDefaultCurrency());
41
     }
31
     }

+ 11
- 3
app/Providers/MacroServiceProvider.php View File

171
 
171
 
172
         Money::macro('swapAmountFor', function ($newCurrency) {
172
         Money::macro('swapAmountFor', function ($newCurrency) {
173
             $oldCurrency = $this->currency->getCurrency();
173
             $oldCurrency = $this->currency->getCurrency();
174
-            $balanceInMajorUnits = $this->getAmount();
174
+            $balanceInSubunits = $this->getAmount();
175
+
176
+            $oldCurrencySubunit = currency($oldCurrency)->getSubunit();
177
+            $newCurrencySubunit = currency($newCurrency)->getSubunit();
178
+
179
+            $balanceInMajorUnits = $balanceInSubunits / $oldCurrencySubunit;
175
 
180
 
176
             $oldRate = currency($oldCurrency)->getRate();
181
             $oldRate = currency($oldCurrency)->getRate();
177
             $newRate = currency($newCurrency)->getRate();
182
             $newRate = currency($newCurrency)->getRate();
178
 
183
 
179
             $ratio = $newRate / $oldRate;
184
             $ratio = $newRate / $oldRate;
185
+            $convertedBalanceInMajorUnits = $balanceInMajorUnits * $ratio;
186
+
187
+            $roundedConvertedBalanceInMajorUnits = round($convertedBalanceInMajorUnits, currency($newCurrency)->getPrecision());
180
 
188
 
181
-            $convertedBalance = bcmul($balanceInMajorUnits, $ratio, 2);
189
+            $convertedBalanceInSubunits = $roundedConvertedBalanceInMajorUnits * $newCurrencySubunit;
182
 
190
 
183
-            return (int) round($convertedBalance);
191
+            return (int) round($convertedBalanceInSubunits);
184
         });
192
         });
185
 
193
 
186
         Money::macro('formatWithCode', function (bool $codeBefore = false) {
194
         Money::macro('formatWithCode', function (bool $codeBefore = false) {

+ 0
- 3
app/Services/TransactionService.php View File

249
     {
249
     {
250
         $defaultCurrency = CurrencyAccessor::getDefaultCurrency();
250
         $defaultCurrency = CurrencyAccessor::getDefaultCurrency();
251
         $bankAccountCurrency = $transaction->bankAccount->account->currency_code;
251
         $bankAccountCurrency = $transaction->bankAccount->account->currency_code;
252
-        $chartAccountCurrency = $transaction->account->currency_code;
253
 
252
 
254
         if ($bankAccountCurrency !== $defaultCurrency) {
253
         if ($bankAccountCurrency !== $defaultCurrency) {
255
             return $this->convertToDefaultCurrency($transaction->amount, $bankAccountCurrency, $defaultCurrency);
254
             return $this->convertToDefaultCurrency($transaction->amount, $bankAccountCurrency, $defaultCurrency);
256
-        } elseif ($chartAccountCurrency !== $defaultCurrency) {
257
-            return $this->convertToDefaultCurrency($transaction->amount, $chartAccountCurrency, $defaultCurrency);
258
         }
255
         }
259
 
256
 
260
         return $transaction->amount;
257
         return $transaction->amount;

+ 22
- 22
composer.lock View File

9922
         },
9922
         },
9923
         {
9923
         {
9924
             "name": "pestphp/pest",
9924
             "name": "pestphp/pest",
9925
-            "version": "v3.2.5",
9925
+            "version": "v3.3.0",
9926
             "source": {
9926
             "source": {
9927
                 "type": "git",
9927
                 "type": "git",
9928
                 "url": "https://github.com/pestphp/pest.git",
9928
                 "url": "https://github.com/pestphp/pest.git",
9929
-                "reference": "1e0bb88b734b3b5999a38fa479683c5dc3ee6f2f"
9929
+                "reference": "0a7bff0d246b10040e12e4152215e12a599e742a"
9930
             },
9930
             },
9931
             "dist": {
9931
             "dist": {
9932
                 "type": "zip",
9932
                 "type": "zip",
9933
-                "url": "https://api.github.com/repos/pestphp/pest/zipball/1e0bb88b734b3b5999a38fa479683c5dc3ee6f2f",
9934
-                "reference": "1e0bb88b734b3b5999a38fa479683c5dc3ee6f2f",
9933
+                "url": "https://api.github.com/repos/pestphp/pest/zipball/0a7bff0d246b10040e12e4152215e12a599e742a",
9934
+                "reference": "0a7bff0d246b10040e12e4152215e12a599e742a",
9935
                 "shasum": ""
9935
                 "shasum": ""
9936
             },
9936
             },
9937
             "require": {
9937
             "require": {
9938
-                "brianium/paratest": "^7.5.5",
9938
+                "brianium/paratest": "^7.5.6",
9939
                 "nunomaduro/collision": "^8.4.0",
9939
                 "nunomaduro/collision": "^8.4.0",
9940
                 "nunomaduro/termwind": "^2.1.0",
9940
                 "nunomaduro/termwind": "^2.1.0",
9941
                 "pestphp/pest-plugin": "^3.0.0",
9941
                 "pestphp/pest-plugin": "^3.0.0",
9942
                 "pestphp/pest-plugin-arch": "^3.0.0",
9942
                 "pestphp/pest-plugin-arch": "^3.0.0",
9943
                 "pestphp/pest-plugin-mutate": "^3.0.5",
9943
                 "pestphp/pest-plugin-mutate": "^3.0.5",
9944
                 "php": "^8.2.0",
9944
                 "php": "^8.2.0",
9945
-                "phpunit/phpunit": "^11.3.6"
9945
+                "phpunit/phpunit": "^11.4.0"
9946
             },
9946
             },
9947
             "conflict": {
9947
             "conflict": {
9948
-                "phpunit/phpunit": ">11.3.6",
9948
+                "phpunit/phpunit": ">11.4.0",
9949
                 "sebastian/exporter": "<6.0.0",
9949
                 "sebastian/exporter": "<6.0.0",
9950
                 "webmozart/assert": "<1.11.0"
9950
                 "webmozart/assert": "<1.11.0"
9951
             },
9951
             },
9952
             "require-dev": {
9952
             "require-dev": {
9953
                 "pestphp/pest-dev-tools": "^3.0.0",
9953
                 "pestphp/pest-dev-tools": "^3.0.0",
9954
-                "pestphp/pest-plugin-type-coverage": "^3.0.1",
9954
+                "pestphp/pest-plugin-type-coverage": "^3.1.0",
9955
                 "symfony/process": "^7.1.5"
9955
                 "symfony/process": "^7.1.5"
9956
             },
9956
             },
9957
             "bin": [
9957
             "bin": [
10017
             ],
10017
             ],
10018
             "support": {
10018
             "support": {
10019
                 "issues": "https://github.com/pestphp/pest/issues",
10019
                 "issues": "https://github.com/pestphp/pest/issues",
10020
-                "source": "https://github.com/pestphp/pest/tree/v3.2.5"
10020
+                "source": "https://github.com/pestphp/pest/tree/v3.3.0"
10021
             },
10021
             },
10022
             "funding": [
10022
             "funding": [
10023
                 {
10023
                 {
10029
                     "type": "github"
10029
                     "type": "github"
10030
                 }
10030
                 }
10031
             ],
10031
             ],
10032
-            "time": "2024-10-01T10:55:18+00:00"
10032
+            "time": "2024-10-06T18:25:27+00:00"
10033
         },
10033
         },
10034
         {
10034
         {
10035
             "name": "pestphp/pest-plugin",
10035
             "name": "pestphp/pest-plugin",
10779
         },
10779
         },
10780
         {
10780
         {
10781
             "name": "phpstan/phpstan",
10781
             "name": "phpstan/phpstan",
10782
-            "version": "1.12.5",
10782
+            "version": "1.12.6",
10783
             "source": {
10783
             "source": {
10784
                 "type": "git",
10784
                 "type": "git",
10785
                 "url": "https://github.com/phpstan/phpstan.git",
10785
                 "url": "https://github.com/phpstan/phpstan.git",
10786
-                "reference": "7e6c6cb7cecb0a6254009a1a8a7d54ec99812b17"
10786
+                "reference": "dc4d2f145a88ea7141ae698effd64d9df46527ae"
10787
             },
10787
             },
10788
             "dist": {
10788
             "dist": {
10789
                 "type": "zip",
10789
                 "type": "zip",
10790
-                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/7e6c6cb7cecb0a6254009a1a8a7d54ec99812b17",
10791
-                "reference": "7e6c6cb7cecb0a6254009a1a8a7d54ec99812b17",
10790
+                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc4d2f145a88ea7141ae698effd64d9df46527ae",
10791
+                "reference": "dc4d2f145a88ea7141ae698effd64d9df46527ae",
10792
                 "shasum": ""
10792
                 "shasum": ""
10793
             },
10793
             },
10794
             "require": {
10794
             "require": {
10833
                     "type": "github"
10833
                     "type": "github"
10834
                 }
10834
                 }
10835
             ],
10835
             ],
10836
-            "time": "2024-09-26T12:45:22+00:00"
10836
+            "time": "2024-10-06T15:03:59+00:00"
10837
         },
10837
         },
10838
         {
10838
         {
10839
             "name": "phpunit/php-code-coverage",
10839
             "name": "phpunit/php-code-coverage",
11160
         },
11160
         },
11161
         {
11161
         {
11162
             "name": "phpunit/phpunit",
11162
             "name": "phpunit/phpunit",
11163
-            "version": "11.3.6",
11163
+            "version": "11.4.0",
11164
             "source": {
11164
             "source": {
11165
                 "type": "git",
11165
                 "type": "git",
11166
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
11166
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
11167
-                "reference": "d62c45a19c665bb872c2a47023a0baf41a98bb2b"
11167
+                "reference": "89fe0c530133c08f7fff89d3d727154e4e504925"
11168
             },
11168
             },
11169
             "dist": {
11169
             "dist": {
11170
                 "type": "zip",
11170
                 "type": "zip",
11171
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d62c45a19c665bb872c2a47023a0baf41a98bb2b",
11172
-                "reference": "d62c45a19c665bb872c2a47023a0baf41a98bb2b",
11171
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/89fe0c530133c08f7fff89d3d727154e4e504925",
11172
+                "reference": "89fe0c530133c08f7fff89d3d727154e4e504925",
11173
                 "shasum": ""
11173
                 "shasum": ""
11174
             },
11174
             },
11175
             "require": {
11175
             "require": {
11208
             "type": "library",
11208
             "type": "library",
11209
             "extra": {
11209
             "extra": {
11210
                 "branch-alias": {
11210
                 "branch-alias": {
11211
-                    "dev-main": "11.3-dev"
11211
+                    "dev-main": "11.4-dev"
11212
                 }
11212
                 }
11213
             },
11213
             },
11214
             "autoload": {
11214
             "autoload": {
11240
             "support": {
11240
             "support": {
11241
                 "issues": "https://github.com/sebastianbergmann/phpunit/issues",
11241
                 "issues": "https://github.com/sebastianbergmann/phpunit/issues",
11242
                 "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
11242
                 "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
11243
-                "source": "https://github.com/sebastianbergmann/phpunit/tree/11.3.6"
11243
+                "source": "https://github.com/sebastianbergmann/phpunit/tree/11.4.0"
11244
             },
11244
             },
11245
             "funding": [
11245
             "funding": [
11246
                 {
11246
                 {
11256
                     "type": "tidelift"
11256
                     "type": "tidelift"
11257
                 }
11257
                 }
11258
             ],
11258
             ],
11259
-            "time": "2024-09-19T10:54:28+00:00"
11259
+            "time": "2024-10-05T08:39:03+00:00"
11260
         },
11260
         },
11261
         {
11261
         {
11262
             "name": "rector/rector",
11262
             "name": "rector/rector",

+ 41
- 1
database/factories/Accounting/AccountFactory.php View File

3
 namespace Database\Factories\Accounting;
3
 namespace Database\Factories\Accounting;
4
 
4
 
5
 use App\Models\Accounting\Account;
5
 use App\Models\Accounting\Account;
6
+use App\Models\Accounting\AccountSubtype;
7
+use App\Models\Banking\BankAccount;
8
+use App\Models\Setting\Currency;
9
+use App\Utilities\Currency\CurrencyAccessor;
6
 use Illuminate\Database\Eloquent\Factories\Factory;
10
 use Illuminate\Database\Eloquent\Factories\Factory;
7
 
11
 
8
 /**
12
 /**
23
     public function definition(): array
27
     public function definition(): array
24
     {
28
     {
25
         return [
29
         return [
26
-            //
30
+            'company_id' => 1,
31
+            'subtype_id' => 1,
32
+            'name' => $this->faker->unique()->word,
33
+            'currency_code' => CurrencyAccessor::getDefaultCurrency() ?? 'USD',
34
+            'description' => $this->faker->sentence,
35
+            'archived' => false,
36
+            'default' => false,
27
         ];
37
         ];
28
     }
38
     }
39
+
40
+    public function withBankAccount(string $name): static
41
+    {
42
+        return $this->state(function (array $attributes) use ($name) {
43
+            $bankAccount = BankAccount::factory()->create();
44
+            $accountSubtype = AccountSubtype::where('name', 'Cash and Cash Equivalents')->first();
45
+
46
+            return [
47
+                'bank_account_id' => $bankAccount->id,
48
+                'subtype_id' => $accountSubtype->id,
49
+                'name' => $name,
50
+            ];
51
+        });
52
+    }
53
+
54
+    public function withForeignBankAccount(string $name, string $currencyCode, float $rate): static
55
+    {
56
+        return $this->state(function (array $attributes) use ($currencyCode, $rate, $name) {
57
+            $currency = Currency::factory()->forCurrency($currencyCode, $rate)->create();
58
+            $bankAccount = BankAccount::factory()->create();
59
+            $accountSubtype = AccountSubtype::where('name', 'Cash and Cash Equivalents')->first();
60
+
61
+            return [
62
+                'bank_account_id' => $bankAccount->id,
63
+                'subtype_id' => $accountSubtype->id,
64
+                'name' => $name,
65
+                'currency_code' => $currency->code,
66
+            ];
67
+        });
68
+    }
29
 }
69
 }

+ 42
- 0
database/factories/Accounting/TransactionFactory.php View File

89
         });
89
         });
90
     }
90
     }
91
 
91
 
92
+    public function forBankAccount(?BankAccount $bankAccount = null): static
93
+    {
94
+        return $this->state(function (array $attributes) use ($bankAccount) {
95
+            $bankAccount = $bankAccount ?? BankAccount::factory()->create();
96
+
97
+            return [
98
+                'bank_account_id' => $bankAccount->id,
99
+            ];
100
+        });
101
+    }
102
+
103
+    public function forDestinationBankAccount(?Account $account = null): static
104
+    {
105
+        return $this->state(function (array $attributes) use ($account) {
106
+            $destinationBankAccount = $account ?? Account::factory()->withBankAccount('Destination Bank Account')->create();
107
+
108
+            return [
109
+                'account_id' => $destinationBankAccount->id,
110
+            ];
111
+        });
112
+    }
113
+
92
     public function forUncategorizedRevenue(): static
114
     public function forUncategorizedRevenue(): static
93
     {
115
     {
94
         return $this->state(function (array $attributes) {
116
         return $this->state(function (array $attributes) {
130
             ];
152
             ];
131
         });
153
         });
132
     }
154
     }
155
+
156
+    public function asJournal(int $amount): static
157
+    {
158
+        return $this->state(function () use ($amount) {
159
+            return [
160
+                'type' => TransactionType::Journal,
161
+                'amount' => $amount,
162
+            ];
163
+        });
164
+    }
165
+
166
+    public function asTransfer(int $amount): static
167
+    {
168
+        return $this->state(function () use ($amount) {
169
+            return [
170
+                'type' => TransactionType::Transfer,
171
+                'amount' => $amount,
172
+            ];
173
+        });
174
+    }
133
 }
175
 }

+ 10
- 1
database/factories/Banking/BankAccountFactory.php View File

2
 
2
 
3
 namespace Database\Factories\Banking;
3
 namespace Database\Factories\Banking;
4
 
4
 
5
+use App\Enums\Banking\BankAccountType;
5
 use App\Models\Banking\BankAccount;
6
 use App\Models\Banking\BankAccount;
6
 use Illuminate\Database\Eloquent\Factories\Factory;
7
 use Illuminate\Database\Eloquent\Factories\Factory;
7
 
8
 
10
  */
11
  */
11
 class BankAccountFactory extends Factory
12
 class BankAccountFactory extends Factory
12
 {
13
 {
14
+    /**
15
+     * The name of the factory's corresponding model.
16
+     */
17
+    protected $model = BankAccount::class;
18
+
13
     /**
19
     /**
14
      * Define the model's default state.
20
      * Define the model's default state.
15
      *
21
      *
18
     public function definition(): array
24
     public function definition(): array
19
     {
25
     {
20
         return [
26
         return [
21
-            //
27
+            'company_id' => 1,
28
+            'type' => BankAccountType::Depository,
29
+            'number' => $this->faker->unique()->numerify(str_repeat('#', 12)),
30
+            'enabled' => false,
22
         ];
31
         ];
23
     }
32
     }
24
 }
33
 }

+ 2
- 2
database/factories/Setting/CurrencyFactory.php View File

40
     /**
40
     /**
41
      * Define a state for a specific currency.
41
      * Define a state for a specific currency.
42
      */
42
      */
43
-    public function forCurrency(string $code): Factory
43
+    public function forCurrency(string $code, ?float $rate = null): static
44
     {
44
     {
45
         $currency = currency($code);
45
         $currency = currency($code);
46
 
46
 
47
         return $this->state([
47
         return $this->state([
48
             'name' => $currency->getName(),
48
             'name' => $currency->getName(),
49
             'code' => $currency->getCurrency(),
49
             'code' => $currency->getCurrency(),
50
-            'rate' => $currency->getRate(),
50
+            'rate' => $rate ?? $currency->getRate(),
51
             'precision' => $currency->getPrecision(),
51
             'precision' => $currency->getPrecision(),
52
             'symbol' => $currency->getSymbol(),
52
             'symbol' => $currency->getSymbol(),
53
             'symbol_first' => $currency->isSymbolFirst(),
53
             'symbol_first' => $currency->isSymbolFirst(),

+ 196
- 0
tests/Feature/Accounting/TransactionTest.php View File

1
+<?php
2
+
3
+use App\Models\Accounting\Account;
4
+use App\Models\Accounting\Transaction;
5
+use App\Utilities\Currency\ConfigureCurrencies;
6
+
7
+it('creates correct journal entries for a deposit transaction', function () {
8
+    $transaction = Transaction::factory()
9
+        ->forDefaultBankAccount()
10
+        ->forUncategorizedRevenue()
11
+        ->asDeposit(1000)
12
+        ->create();
13
+
14
+    [$debitAccount, $creditAccount] = getTransactionDebitAndCreditAccounts($transaction);
15
+
16
+    expect($transaction->journalEntries->count())->toBe(2)
17
+        ->and($debitAccount->name)->toBe('Cash on Hand')
18
+        ->and($creditAccount->name)->toBe('Uncategorized Income');
19
+});
20
+
21
+it('creates correct journal entries for a withdrawal transaction', function () {
22
+    $transaction = Transaction::factory()
23
+        ->forDefaultBankAccount()
24
+        ->forUncategorizedExpense()
25
+        ->asWithdrawal(500)
26
+        ->create();
27
+
28
+    [$debitAccount, $creditAccount] = getTransactionDebitAndCreditAccounts($transaction);
29
+
30
+    expect($transaction->journalEntries->count())->toBe(2)
31
+        ->and($debitAccount->name)->toBe('Uncategorized Expense')
32
+        ->and($creditAccount->name)->toBe('Cash on Hand');
33
+});
34
+
35
+it('creates correct journal entries for a transfer transaction', function () {
36
+    $transaction = Transaction::factory()
37
+        ->forDefaultBankAccount()
38
+        ->forDestinationBankAccount()
39
+        ->asTransfer(1500)
40
+        ->create();
41
+
42
+    [$debitAccount, $creditAccount] = getTransactionDebitAndCreditAccounts($transaction);
43
+
44
+    // Acts as a withdrawal transaction for the source account
45
+    expect($transaction->journalEntries->count())->toBe(2)
46
+        ->and($debitAccount->name)->toBe('Destination Bank Account')
47
+        ->and($creditAccount->name)->toBe('Cash on Hand');
48
+});
49
+
50
+it('does not create journal entries for a journal transaction', function () {
51
+    $transaction = Transaction::factory()
52
+        ->forDefaultBankAccount()
53
+        ->forUncategorizedRevenue()
54
+        ->asJournal(1000)
55
+        ->create();
56
+
57
+    // Journal entries for a journal transaction are created manually
58
+    expect($transaction->journalEntries->count())->toBe(0);
59
+});
60
+
61
+it('stores and sums correct debit and credit amounts for different transaction types', function ($method, $setupMethod, $amount) {
62
+    $transaction = Transaction::factory()
63
+        ->forDefaultBankAccount()
64
+        ->{$setupMethod}()
65
+        ->{$method}($amount)
66
+        ->create();
67
+
68
+    expect($transaction->journalEntries->sumDebits()->getValue())->toEqual($amount)
69
+        ->and($transaction->journalEntries->sumCredits()->getValue())->toEqual($amount);
70
+})->with([
71
+    ['asDeposit', 'forUncategorizedRevenue', 2000],
72
+    ['asWithdrawal', 'forUncategorizedExpense', 500],
73
+    ['asTransfer', 'forDestinationBankAccount', 1500],
74
+]);
75
+
76
+it('deletes associated journal entries when transaction is deleted', function () {
77
+    $transaction = Transaction::factory()
78
+        ->forDefaultBankAccount()
79
+        ->forUncategorizedRevenue()
80
+        ->asDeposit(1000)
81
+        ->create();
82
+
83
+    expect($transaction->journalEntries()->count())->toBe(2);
84
+
85
+    $transaction->delete();
86
+
87
+    $this->assertModelMissing($transaction);
88
+
89
+    $this->assertDatabaseCount('journal_entries', 0);
90
+});
91
+
92
+it('handles multi-currency transfers without conversion when the source bank account is in the default currency', function () {
93
+    $foreignBankAccount = Account::factory()
94
+        ->withForeignBankAccount('Foreign Bank Account', 'EUR', 0.92)
95
+        ->create();
96
+
97
+    $transaction = Transaction::factory()
98
+        ->forDefaultBankAccount()
99
+        ->forDestinationBankAccount($foreignBankAccount)
100
+        ->asTransfer(1500)
101
+        ->create();
102
+
103
+    [$debitAccount, $creditAccount] = getTransactionDebitAndCreditAccounts($transaction);
104
+
105
+    expect($transaction->journalEntries->count())->toBe(2)
106
+        ->and($debitAccount->name)->toBe('Foreign Bank Account')
107
+        ->and($creditAccount->name)->toBe('Cash on Hand')
108
+        ->and($transaction->journalEntries->sumDebits()->getValue())->toEqual(1500)
109
+        ->and($transaction->journalEntries->sumCredits()->getValue())->toEqual(1500)
110
+        ->and($transaction->amount)->toEqual('1,500.00');
111
+});
112
+
113
+it('handles multi-currency transfers correctly', function () {
114
+    $foreignBankAccount = Account::factory()
115
+        ->withForeignBankAccount('CAD Bank Account', 'CAD', 1.36)
116
+        ->create();
117
+
118
+    ConfigureCurrencies::syncCurrencies();
119
+
120
+    // Create a transfer of 1500 CAD from the foreign bank account to USD bank account
121
+    $transaction = Transaction::factory()
122
+        ->forBankAccount($foreignBankAccount->bankAccount)
123
+        ->forDestinationBankAccount()
124
+        ->asTransfer(1500)
125
+        ->create();
126
+
127
+    [$debitAccount, $creditAccount] = getTransactionDebitAndCreditAccounts($transaction);
128
+
129
+    expect($transaction->journalEntries->count())->toBe(2)
130
+        ->and($debitAccount->name)->toBe('Destination Bank Account') // Debit: Destination (USD) account
131
+        ->and($creditAccount->name)->toBe('CAD Bank Account'); // Credit: Foreign (CAD) account
132
+
133
+    // The 1500 CAD is worth 1102.94 USD (1500 CAD / 1.36)
134
+    $expectedUSDValue = round(1500 / 1.36, 2);
135
+
136
+    // Verify that the debit is 1102.94 USD and the credit is 1500 CAD converted to 1102.94 USD
137
+    // Transaction amount stays in source bank account currency (cast is applied)
138
+    expect($transaction->journalEntries->sumDebits()->getValue())->toEqual($expectedUSDValue)
139
+        ->and($transaction->journalEntries->sumCredits()->getValue())->toEqual($expectedUSDValue)
140
+        ->and($transaction->amount)->toEqual('1,500.00');
141
+});
142
+
143
+it('handles multi-currency deposits correctly', function () {
144
+    $foreignBankAccount = Account::factory()
145
+        ->withForeignBankAccount('BHD Bank Account', 'BHD', 0.38)
146
+        ->create();
147
+
148
+    ConfigureCurrencies::syncCurrencies();
149
+
150
+    // Create a deposit of 1500 BHD to the foreign bank account
151
+    $transaction = Transaction::factory()
152
+        ->forBankAccount($foreignBankAccount->bankAccount)
153
+        ->forUncategorizedRevenue()
154
+        ->asDeposit(1500)
155
+        ->create();
156
+
157
+    [$debitAccount, $creditAccount] = getTransactionDebitAndCreditAccounts($transaction);
158
+
159
+    expect($transaction->journalEntries->count())->toBe(2)
160
+        ->and($debitAccount->name)->toBe('BHD Bank Account') // Debit: Foreign (BHD) account
161
+        ->and($creditAccount->name)->toBe('Uncategorized Income'); // Credit: Uncategorized Income (USD) account
162
+
163
+    // Convert to USD using the rate 0.38 BHD per USD
164
+    $expectedUSDValue = round(1500 / 0.38, 2);
165
+
166
+    // Verify that the debit is 39473.68 USD and the credit is 1500 BHD converted to 39473.68 USD
167
+    expect($transaction->journalEntries->sumDebits()->getValue())->toEqual($expectedUSDValue)
168
+        ->and($transaction->journalEntries->sumCredits()->getValue())->toEqual($expectedUSDValue)
169
+        ->and($transaction->amount)->toEqual('1,500.000'); // Original amount in BHD (3 decimal precision)
170
+});
171
+
172
+it('handles multi-currency withdrawals correctly', function () {
173
+    $foreignBankAccount = Account::factory()
174
+        ->withForeignBankAccount('Foreign Bank Account', 'GBP', 0.76) // GBP account
175
+        ->create();
176
+
177
+    ConfigureCurrencies::syncCurrencies();
178
+
179
+    $transaction = Transaction::factory()
180
+        ->forBankAccount($foreignBankAccount->bankAccount)
181
+        ->forUncategorizedExpense()
182
+        ->asWithdrawal(1500)
183
+        ->create();
184
+
185
+    [$debitAccount, $creditAccount] = getTransactionDebitAndCreditAccounts($transaction);
186
+
187
+    expect($transaction->journalEntries->count())->toBe(2)
188
+        ->and($debitAccount->name)->toBe('Uncategorized Expense')
189
+        ->and($creditAccount->name)->toBe('Foreign Bank Account');
190
+
191
+    $expectedUSDValue = round(1500 / 0.76, 2);
192
+
193
+    expect($transaction->journalEntries->sumDebits()->getValue())->toEqual($expectedUSDValue)
194
+        ->and($transaction->journalEntries->sumCredits()->getValue())->toEqual($expectedUSDValue)
195
+        ->and($transaction->amount)->toEqual('1,500.00');
196
+});

+ 0
- 7
tests/Feature/ExampleTest.php View File

1
-<?php
2
-
3
-it('returns a successful response', function () {
4
-    $response = $this->get('/');
5
-
6
-    $response->assertStatus(200);
7
-});

+ 16
- 0
tests/Helpers/helpers.php View File

1
 <?php
1
 <?php
2
 
2
 
3
+use App\Enums\Accounting\JournalEntryType;
3
 use App\Enums\Setting\EntityType;
4
 use App\Enums\Setting\EntityType;
4
 use App\Filament\Company\Pages\CreateCompany;
5
 use App\Filament\Company\Pages\CreateCompany;
6
+use App\Models\Accounting\Account;
7
+use App\Models\Accounting\Transaction;
5
 use App\Models\Company;
8
 use App\Models\Company;
6
 
9
 
7
 use function Pest\Livewire\livewire;
10
 use function Pest\Livewire\livewire;
22
 
25
 
23
     return auth()->user()->currentCompany;
26
     return auth()->user()->currentCompany;
24
 }
27
 }
28
+
29
+/**
30
+ * Get the debit and credit accounts for a transaction.
31
+ *
32
+ * @return array<Account>
33
+ */
34
+function getTransactionDebitAndCreditAccounts(Transaction $transaction): array
35
+{
36
+    $debitAccount = $transaction->journalEntries->where('type', JournalEntryType::Debit)->firstOrFail()->account;
37
+    $creditAccount = $transaction->journalEntries->where('type', JournalEntryType::Credit)->firstOrFail()->account;
38
+
39
+    return [$debitAccount, $creditAccount];
40
+}

Loading…
Cancel
Save