浏览代码

wip testing

3.x
Andrew Wallo 1年前
父节点
当前提交
3ee921e8ee

+ 9
- 19
app/Collections/Accounting/JournalEntryCollection.php 查看文件

@@ -2,9 +2,9 @@
2 2
 
3 3
 namespace App\Collections\Accounting;
4 4
 
5
+use App\Enums\Accounting\JournalEntryType;
5 6
 use App\Models\Accounting\JournalEntry;
6 7
 use App\Utilities\Currency\CurrencyAccessor;
7
-use App\Utilities\Currency\CurrencyConverter;
8 8
 use App\ValueObjects\Money;
9 9
 use Illuminate\Database\Eloquent\Collection;
10 10
 
@@ -12,30 +12,20 @@ class JournalEntryCollection extends Collection
12 12
 {
13 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 20
         return new Money($total, CurrencyAccessor::getDefaultCurrency());
26 21
     }
27 22
 
28 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 30
         return new Money($total, CurrencyAccessor::getDefaultCurrency());
41 31
     }

+ 11
- 3
app/Providers/MacroServiceProvider.php 查看文件

@@ -171,16 +171,24 @@ class MacroServiceProvider extends ServiceProvider
171 171
 
172 172
         Money::macro('swapAmountFor', function ($newCurrency) {
173 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 181
             $oldRate = currency($oldCurrency)->getRate();
177 182
             $newRate = currency($newCurrency)->getRate();
178 183
 
179 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 194
         Money::macro('formatWithCode', function (bool $codeBefore = false) {

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

@@ -249,12 +249,9 @@ class TransactionService
249 249
     {
250 250
         $defaultCurrency = CurrencyAccessor::getDefaultCurrency();
251 251
         $bankAccountCurrency = $transaction->bankAccount->account->currency_code;
252
-        $chartAccountCurrency = $transaction->account->currency_code;
253 252
 
254 253
         if ($bankAccountCurrency !== $defaultCurrency) {
255 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 257
         return $transaction->amount;

+ 22
- 22
composer.lock 查看文件

@@ -9922,36 +9922,36 @@
9922 9922
         },
9923 9923
         {
9924 9924
             "name": "pestphp/pest",
9925
-            "version": "v3.2.5",
9925
+            "version": "v3.3.0",
9926 9926
             "source": {
9927 9927
                 "type": "git",
9928 9928
                 "url": "https://github.com/pestphp/pest.git",
9929
-                "reference": "1e0bb88b734b3b5999a38fa479683c5dc3ee6f2f"
9929
+                "reference": "0a7bff0d246b10040e12e4152215e12a599e742a"
9930 9930
             },
9931 9931
             "dist": {
9932 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 9935
                 "shasum": ""
9936 9936
             },
9937 9937
             "require": {
9938
-                "brianium/paratest": "^7.5.5",
9938
+                "brianium/paratest": "^7.5.6",
9939 9939
                 "nunomaduro/collision": "^8.4.0",
9940 9940
                 "nunomaduro/termwind": "^2.1.0",
9941 9941
                 "pestphp/pest-plugin": "^3.0.0",
9942 9942
                 "pestphp/pest-plugin-arch": "^3.0.0",
9943 9943
                 "pestphp/pest-plugin-mutate": "^3.0.5",
9944 9944
                 "php": "^8.2.0",
9945
-                "phpunit/phpunit": "^11.3.6"
9945
+                "phpunit/phpunit": "^11.4.0"
9946 9946
             },
9947 9947
             "conflict": {
9948
-                "phpunit/phpunit": ">11.3.6",
9948
+                "phpunit/phpunit": ">11.4.0",
9949 9949
                 "sebastian/exporter": "<6.0.0",
9950 9950
                 "webmozart/assert": "<1.11.0"
9951 9951
             },
9952 9952
             "require-dev": {
9953 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 9955
                 "symfony/process": "^7.1.5"
9956 9956
             },
9957 9957
             "bin": [
@@ -10017,7 +10017,7 @@
10017 10017
             ],
10018 10018
             "support": {
10019 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 10022
             "funding": [
10023 10023
                 {
@@ -10029,7 +10029,7 @@
10029 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 10035
             "name": "pestphp/pest-plugin",
@@ -10779,16 +10779,16 @@
10779 10779
         },
10780 10780
         {
10781 10781
             "name": "phpstan/phpstan",
10782
-            "version": "1.12.5",
10782
+            "version": "1.12.6",
10783 10783
             "source": {
10784 10784
                 "type": "git",
10785 10785
                 "url": "https://github.com/phpstan/phpstan.git",
10786
-                "reference": "7e6c6cb7cecb0a6254009a1a8a7d54ec99812b17"
10786
+                "reference": "dc4d2f145a88ea7141ae698effd64d9df46527ae"
10787 10787
             },
10788 10788
             "dist": {
10789 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 10792
                 "shasum": ""
10793 10793
             },
10794 10794
             "require": {
@@ -10833,7 +10833,7 @@
10833 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 10839
             "name": "phpunit/php-code-coverage",
@@ -11160,16 +11160,16 @@
11160 11160
         },
11161 11161
         {
11162 11162
             "name": "phpunit/phpunit",
11163
-            "version": "11.3.6",
11163
+            "version": "11.4.0",
11164 11164
             "source": {
11165 11165
                 "type": "git",
11166 11166
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
11167
-                "reference": "d62c45a19c665bb872c2a47023a0baf41a98bb2b"
11167
+                "reference": "89fe0c530133c08f7fff89d3d727154e4e504925"
11168 11168
             },
11169 11169
             "dist": {
11170 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 11173
                 "shasum": ""
11174 11174
             },
11175 11175
             "require": {
@@ -11208,7 +11208,7 @@
11208 11208
             "type": "library",
11209 11209
             "extra": {
11210 11210
                 "branch-alias": {
11211
-                    "dev-main": "11.3-dev"
11211
+                    "dev-main": "11.4-dev"
11212 11212
                 }
11213 11213
             },
11214 11214
             "autoload": {
@@ -11240,7 +11240,7 @@
11240 11240
             "support": {
11241 11241
                 "issues": "https://github.com/sebastianbergmann/phpunit/issues",
11242 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 11245
             "funding": [
11246 11246
                 {
@@ -11256,7 +11256,7 @@
11256 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 11262
             "name": "rector/rector",

+ 41
- 1
database/factories/Accounting/AccountFactory.php 查看文件

@@ -3,6 +3,10 @@
3 3
 namespace Database\Factories\Accounting;
4 4
 
5 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 10
 use Illuminate\Database\Eloquent\Factories\Factory;
7 11
 
8 12
 /**
@@ -23,7 +27,43 @@ class AccountFactory extends Factory
23 27
     public function definition(): array
24 28
     {
25 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 查看文件

@@ -89,6 +89,28 @@ class TransactionFactory extends Factory
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 114
     public function forUncategorizedRevenue(): static
93 115
     {
94 116
         return $this->state(function (array $attributes) {
@@ -130,4 +152,24 @@ class TransactionFactory extends Factory
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 查看文件

@@ -2,6 +2,7 @@
2 2
 
3 3
 namespace Database\Factories\Banking;
4 4
 
5
+use App\Enums\Banking\BankAccountType;
5 6
 use App\Models\Banking\BankAccount;
6 7
 use Illuminate\Database\Eloquent\Factories\Factory;
7 8
 
@@ -10,6 +11,11 @@ use Illuminate\Database\Eloquent\Factories\Factory;
10 11
  */
11 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 20
      * Define the model's default state.
15 21
      *
@@ -18,7 +24,10 @@ class BankAccountFactory extends Factory
18 24
     public function definition(): array
19 25
     {
20 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 查看文件

@@ -40,14 +40,14 @@ class CurrencyFactory extends Factory
40 40
     /**
41 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 45
         $currency = currency($code);
46 46
 
47 47
         return $this->state([
48 48
             'name' => $currency->getName(),
49 49
             'code' => $currency->getCurrency(),
50
-            'rate' => $currency->getRate(),
50
+            'rate' => $rate ?? $currency->getRate(),
51 51
             'precision' => $currency->getPrecision(),
52 52
             'symbol' => $currency->getSymbol(),
53 53
             'symbol_first' => $currency->isSymbolFirst(),

+ 196
- 0
tests/Feature/Accounting/TransactionTest.php 查看文件

@@ -0,0 +1,196 @@
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 查看文件

@@ -1,7 +0,0 @@
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 查看文件

@@ -1,7 +1,10 @@
1 1
 <?php
2 2
 
3
+use App\Enums\Accounting\JournalEntryType;
3 4
 use App\Enums\Setting\EntityType;
4 5
 use App\Filament\Company\Pages\CreateCompany;
6
+use App\Models\Accounting\Account;
7
+use App\Models\Accounting\Transaction;
5 8
 use App\Models\Company;
6 9
 
7 10
 use function Pest\Livewire\livewire;
@@ -22,3 +25,16 @@ function createCompany(string $name): Company
22 25
 
23 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
+}

正在加载...
取消
保存