Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

TransactionTest.php 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  1. <?php
  2. use App\Enums\Accounting\JournalEntryType;
  3. use App\Enums\Accounting\TransactionType;
  4. use App\Filament\Company\Pages\Accounting\Transactions;
  5. use App\Filament\Forms\Components\JournalEntryRepeater;
  6. use App\Filament\Tables\Actions\ReplicateBulkAction;
  7. use App\Models\Accounting\Account;
  8. use App\Models\Accounting\Transaction;
  9. use App\Utilities\Currency\ConfigureCurrencies;
  10. use Filament\Tables\Actions\DeleteAction;
  11. use Filament\Tables\Actions\DeleteBulkAction;
  12. use Filament\Tables\Actions\ReplicateAction;
  13. use function Pest\Livewire\livewire;
  14. it('creates correct journal entries for a deposit transaction', function () {
  15. $transaction = Transaction::factory()
  16. ->forDefaultBankAccount()
  17. ->forUncategorizedRevenue()
  18. ->asDeposit(1000)
  19. ->create();
  20. [$debitAccount, $creditAccount] = getTransactionDebitAndCreditAccounts($transaction);
  21. expect($transaction->journalEntries->count())->toBe(2)
  22. ->and($debitAccount->name)->toBe('Cash on Hand')
  23. ->and($creditAccount->name)->toBe('Uncategorized Income');
  24. });
  25. it('creates correct journal entries for a withdrawal transaction', function () {
  26. $transaction = Transaction::factory()
  27. ->forDefaultBankAccount()
  28. ->forUncategorizedExpense()
  29. ->asWithdrawal(500)
  30. ->create();
  31. [$debitAccount, $creditAccount] = getTransactionDebitAndCreditAccounts($transaction);
  32. expect($transaction->journalEntries->count())->toBe(2)
  33. ->and($debitAccount->name)->toBe('Uncategorized Expense')
  34. ->and($creditAccount->name)->toBe('Cash on Hand');
  35. });
  36. it('creates correct journal entries for a transfer transaction', function () {
  37. $transaction = Transaction::factory()
  38. ->forDefaultBankAccount()
  39. ->forDestinationBankAccount()
  40. ->asTransfer(1500)
  41. ->create();
  42. [$debitAccount, $creditAccount] = getTransactionDebitAndCreditAccounts($transaction);
  43. // Acts as a withdrawal transaction for the source account
  44. expect($transaction->journalEntries->count())->toBe(2)
  45. ->and($debitAccount->name)->toBe('Destination Bank Account')
  46. ->and($creditAccount->name)->toBe('Cash on Hand');
  47. });
  48. it('does not create journal entries for a journal transaction', function () {
  49. $transaction = Transaction::factory()
  50. ->forDefaultBankAccount()
  51. ->forUncategorizedRevenue()
  52. ->asJournal(1000)
  53. ->create();
  54. // Journal entries for a journal transaction are created manually
  55. expect($transaction->journalEntries->count())->toBe(0);
  56. });
  57. it('stores and sums correct debit and credit amounts for different transaction types', function ($method, $setupMethod, $amount) {
  58. /** @var Transaction $transaction */
  59. $transaction = Transaction::factory()
  60. ->forDefaultBankAccount()
  61. ->{$setupMethod}()
  62. ->{$method}($amount)
  63. ->create();
  64. expect($transaction)
  65. ->journalEntries->sumDebits()->getValue()->toEqual($amount)
  66. ->journalEntries->sumCredits()->getValue()->toEqual($amount);
  67. })->with([
  68. ['asDeposit', 'forUncategorizedRevenue', 2000],
  69. ['asWithdrawal', 'forUncategorizedExpense', 500],
  70. ['asTransfer', 'forDestinationBankAccount', 1500],
  71. ]);
  72. it('deletes associated journal entries when transaction is deleted', function () {
  73. $transaction = Transaction::factory()
  74. ->forDefaultBankAccount()
  75. ->forUncategorizedRevenue()
  76. ->asDeposit(1000)
  77. ->create();
  78. expect($transaction->journalEntries()->count())->toBe(2);
  79. $transaction->delete();
  80. $this->assertModelMissing($transaction);
  81. $this->assertDatabaseCount('journal_entries', 0);
  82. });
  83. it('handles multi-currency transfers without conversion when the source bank account is in the default currency', function () {
  84. $foreignBankAccount = Account::factory()
  85. ->withForeignBankAccount('Foreign Bank Account', 'EUR', 0.92)
  86. ->create();
  87. /** @var Transaction $transaction */
  88. $transaction = Transaction::factory()
  89. ->forDefaultBankAccount()
  90. ->forDestinationBankAccount($foreignBankAccount)
  91. ->asTransfer(1500)
  92. ->create();
  93. [$debitAccount, $creditAccount] = getTransactionDebitAndCreditAccounts($transaction);
  94. expect($debitAccount->is($foreignBankAccount))->toBeTrue()
  95. ->and($creditAccount->name)->toBe('Cash on Hand');
  96. $expectedUSDValue = 1500;
  97. expect($transaction)
  98. ->amount->toEqual('1,500.00')
  99. ->journalEntries->count()->toBe(2)
  100. ->journalEntries->sumDebits()->getValue()->toEqual($expectedUSDValue)
  101. ->journalEntries->sumCredits()->getValue()->toEqual($expectedUSDValue);
  102. });
  103. it('handles multi-currency transfers correctly', function () {
  104. $foreignBankAccount = Account::factory()
  105. ->withForeignBankAccount('CAD Bank Account', 'CAD', 1.36)
  106. ->create();
  107. ConfigureCurrencies::syncCurrencies();
  108. // Create a transfer of 1500 CAD from the foreign bank account to USD bank account
  109. /** @var Transaction $transaction */
  110. $transaction = Transaction::factory()
  111. ->forBankAccount($foreignBankAccount->bankAccount)
  112. ->forDestinationBankAccount()
  113. ->asTransfer(1500)
  114. ->create();
  115. [$debitAccount, $creditAccount] = getTransactionDebitAndCreditAccounts($transaction);
  116. expect($debitAccount->name)->toBe('Destination Bank Account') // Debit: Destination (USD) account
  117. ->and($creditAccount->is($foreignBankAccount))->toBeTrue(); // Credit: Foreign Bank Account (CAD) account
  118. // The 1500 CAD is worth 1102.94 USD (1500 CAD / 1.36)
  119. $expectedUSDValue = round(1500 / 1.36, 2);
  120. // Verify that the debit is 1102.94 USD and the credit is 1500 CAD converted to 1102.94 USD
  121. // Transaction amount stays in source bank account currency (cast is applied)
  122. expect($transaction)
  123. ->amount->toEqual('1,500.00')
  124. ->journalEntries->count()->toBe(2)
  125. ->journalEntries->sumDebits()->getValue()->toEqual($expectedUSDValue)
  126. ->journalEntries->sumCredits()->getValue()->toEqual($expectedUSDValue);
  127. });
  128. it('handles multi-currency deposits correctly', function () {
  129. $foreignBankAccount = Account::factory()
  130. ->withForeignBankAccount('BHD Bank Account', 'BHD', 0.38)
  131. ->create();
  132. ConfigureCurrencies::syncCurrencies();
  133. // Create a deposit of 1500 BHD to the foreign bank account
  134. /** @var Transaction $transaction */
  135. $transaction = Transaction::factory()
  136. ->forBankAccount($foreignBankAccount->bankAccount)
  137. ->forUncategorizedRevenue()
  138. ->asDeposit(1500)
  139. ->create();
  140. [$debitAccount, $creditAccount] = getTransactionDebitAndCreditAccounts($transaction);
  141. expect($debitAccount->is($foreignBankAccount))->toBeTrue() // Debit: Foreign Bank Account (BHD) account
  142. ->and($creditAccount->name)->toBe('Uncategorized Income'); // Credit: Uncategorized Income (USD) account
  143. // Convert to USD using the rate 0.38 BHD per USD
  144. $expectedUSDValue = round(1500 / 0.38, 2);
  145. // Verify that the debit is 39473.68 USD and the credit is 1500 BHD converted to 39473.68 USD
  146. expect($transaction)
  147. ->amount->toEqual('1,500.000')
  148. ->journalEntries->count()->toBe(2)
  149. ->journalEntries->sumDebits()->getValue()->toEqual($expectedUSDValue)
  150. ->journalEntries->sumCredits()->getValue()->toEqual($expectedUSDValue);
  151. });
  152. it('handles multi-currency withdrawals correctly', function () {
  153. $foreignBankAccount = Account::factory()
  154. ->withForeignBankAccount('Foreign Bank Account', 'GBP', 0.76) // GBP account
  155. ->create();
  156. ConfigureCurrencies::syncCurrencies();
  157. /** @var Transaction $transaction */
  158. $transaction = Transaction::factory()
  159. ->forBankAccount($foreignBankAccount->bankAccount)
  160. ->forUncategorizedExpense()
  161. ->asWithdrawal(1500)
  162. ->create();
  163. [$debitAccount, $creditAccount] = getTransactionDebitAndCreditAccounts($transaction);
  164. expect($debitAccount->name)->toBe('Uncategorized Expense')
  165. ->and($creditAccount->is($foreignBankAccount))->toBeTrue();
  166. $expectedUSDValue = round(1500 / 0.76, 2);
  167. expect($transaction)
  168. ->amount->toEqual('1,500.00')
  169. ->journalEntries->count()->toBe(2)
  170. ->journalEntries->sumDebits()->getValue()->toEqual($expectedUSDValue)
  171. ->journalEntries->sumCredits()->getValue()->toEqual($expectedUSDValue);
  172. });
  173. it('can add an income or expense transaction', function (TransactionType $transactionType, string $actionName) {
  174. $testCompany = $this->testCompany;
  175. $defaultBankAccount = $testCompany->default->bankAccount;
  176. $defaultAccount = Transactions::getUncategorizedAccountByType($transactionType);
  177. livewire(Transactions::class)
  178. ->mountAction($actionName)
  179. ->assertActionDataSet([
  180. 'posted_at' => now()->toDateTimeString(),
  181. 'type' => $transactionType,
  182. 'bank_account_id' => $defaultBankAccount->id,
  183. 'amount' => '0.00',
  184. 'account_id' => $defaultAccount->id,
  185. ])
  186. ->setActionData([
  187. 'amount' => '500.00',
  188. ])
  189. ->callMountedAction()
  190. ->assertHasNoActionErrors();
  191. $transaction = Transaction::first();
  192. expect($transaction)
  193. ->not->toBeNull()
  194. ->amount->toEqual('500.00')
  195. ->type->toBe($transactionType)
  196. ->bankAccount->is($defaultBankAccount)->toBeTrue()
  197. ->account->is($defaultAccount)->toBeTrue()
  198. ->journalEntries->count()->toBe(2);
  199. })->with([
  200. [TransactionType::Deposit, 'addIncome'],
  201. [TransactionType::Withdrawal, 'addExpense'],
  202. ]);
  203. it('can add a transfer transaction', function () {
  204. $testCompany = $this->testCompany;
  205. $sourceBankAccount = $testCompany->default->bankAccount;
  206. $destinationBankAccount = Account::factory()->withBankAccount('Destination Bank Account')->create();
  207. livewire(Transactions::class)
  208. ->mountAction('addTransfer')
  209. ->assertActionDataSet([
  210. 'posted_at' => now()->toDateTimeString(),
  211. 'type' => TransactionType::Transfer,
  212. 'bank_account_id' => $sourceBankAccount->id,
  213. 'amount' => '0.00',
  214. 'account_id' => null,
  215. ])
  216. ->setActionData([
  217. 'account_id' => $destinationBankAccount->id,
  218. 'amount' => '1,500.00',
  219. ])
  220. ->callMountedAction()
  221. ->assertHasNoActionErrors();
  222. $transaction = Transaction::first();
  223. expect($transaction)
  224. ->not->toBeNull()
  225. ->amount->toEqual('1,500.00')
  226. ->type->toBe(TransactionType::Transfer)
  227. ->bankAccount->is($sourceBankAccount)->toBeTrue()
  228. ->account->is($destinationBankAccount)->toBeTrue()
  229. ->journalEntries->count()->toBe(2);
  230. });
  231. it('can add a journal transaction', function () {
  232. $defaultDebitAccount = Transactions::getUncategorizedAccountByType(TransactionType::Withdrawal);
  233. $defaultCreditAccount = Transactions::getUncategorizedAccountByType(TransactionType::Deposit);
  234. $undoRepeaterFake = JournalEntryRepeater::fake();
  235. livewire(Transactions::class)
  236. ->mountAction('addJournalTransaction')
  237. ->assertActionDataSet([
  238. 'posted_at' => now()->toDateTimeString(),
  239. 'journalEntries' => [
  240. ['type' => JournalEntryType::Debit, 'account_id' => $defaultDebitAccount->id, 'amount' => '0.00'],
  241. ['type' => JournalEntryType::Credit, 'account_id' => $defaultCreditAccount->id, 'amount' => '0.00'],
  242. ],
  243. ])
  244. ->setActionData([
  245. 'journalEntries' => [
  246. ['amount' => '1,000.00'],
  247. ['amount' => '1,000.00'],
  248. ],
  249. ])
  250. ->callMountedAction()
  251. ->assertHasNoActionErrors();
  252. $undoRepeaterFake();
  253. $transaction = Transaction::first();
  254. [$debitAccount, $creditAccount] = getTransactionDebitAndCreditAccounts($transaction);
  255. expect($transaction)
  256. ->not->toBeNull()
  257. ->amount->toEqual('1,000.00')
  258. ->type->isJournal()->toBeTrue()
  259. ->bankAccount->toBeNull()
  260. ->account->toBeNull()
  261. ->journalEntries->count()->toBe(2)
  262. ->journalEntries->sumDebits()->getValue()->toEqual(1000)
  263. ->journalEntries->sumCredits()->getValue()->toEqual(1000)
  264. ->and($debitAccount->is($defaultDebitAccount))->toBeTrue()
  265. ->and($creditAccount->is($defaultCreditAccount))->toBeTrue();
  266. });
  267. it('can update a deposit or withdrawal transaction', function (TransactionType $transactionType) {
  268. $defaultAccount = Transactions::getUncategorizedAccountByType($transactionType);
  269. $transaction = Transaction::factory()
  270. ->forDefaultBankAccount()
  271. ->forAccount($defaultAccount)
  272. ->forType($transactionType, 1000)
  273. ->create();
  274. $newDescription = 'Updated Description';
  275. livewire(Transactions::class)
  276. ->mountTableAction('updateTransaction', $transaction)
  277. ->assertTableActionDataSet([
  278. 'type' => $transactionType->value,
  279. 'description' => $transaction->description,
  280. 'amount' => $transaction->amount,
  281. ])
  282. ->setTableActionData([
  283. 'description' => $newDescription,
  284. 'amount' => '1,500.00',
  285. ])
  286. ->callMountedTableAction()
  287. ->assertHasNoTableActionErrors();
  288. $transaction->refresh();
  289. expect($transaction->description)->toBe($newDescription)
  290. ->and($transaction->amount)->toEqual('1,500.00');
  291. })->with([
  292. TransactionType::Deposit,
  293. TransactionType::Withdrawal,
  294. ]);
  295. it('does not show Edit Transfer or Edit Journal Transaction for deposit or withdrawal transactions', function (TransactionType $transactionType) {
  296. $defaultAccount = Transactions::getUncategorizedAccountByType($transactionType);
  297. $transaction = Transaction::factory()
  298. ->forDefaultBankAccount()
  299. ->forAccount($defaultAccount)
  300. ->forType($transactionType, 1000)
  301. ->create();
  302. livewire(Transactions::class)
  303. ->assertTableActionHidden('updateTransfer', $transaction)
  304. ->assertTableActionHidden('updateJournalTransaction', $transaction);
  305. })->with([
  306. TransactionType::Deposit,
  307. TransactionType::Withdrawal,
  308. ]);
  309. it('can update a transfer transaction', function () {
  310. $transaction = Transaction::factory()
  311. ->forDefaultBankAccount()
  312. ->forDestinationBankAccount()
  313. ->asTransfer(1500)
  314. ->create();
  315. $newDescription = 'Updated Transfer Description';
  316. livewire(Transactions::class)
  317. ->mountTableAction('updateTransfer', $transaction)
  318. ->assertTableActionDataSet([
  319. 'type' => TransactionType::Transfer->value,
  320. 'description' => $transaction->description,
  321. 'amount' => $transaction->amount,
  322. ])
  323. ->setTableActionData([
  324. 'description' => $newDescription,
  325. 'amount' => '2,000.00',
  326. ])
  327. ->callMountedTableAction()
  328. ->assertHasNoTableActionErrors();
  329. $transaction->refresh();
  330. expect($transaction->description)->toBe($newDescription)
  331. ->and($transaction->amount)->toEqual('2,000.00');
  332. });
  333. it('does not show Edit Transaction or Edit Journal Transaction for transfer transactions', function () {
  334. $transaction = Transaction::factory()
  335. ->forDefaultBankAccount()
  336. ->forDestinationBankAccount()
  337. ->asTransfer(1500)
  338. ->create();
  339. livewire(Transactions::class)
  340. ->assertTableActionHidden('updateTransaction', $transaction)
  341. ->assertTableActionHidden('updateJournalTransaction', $transaction);
  342. });
  343. it('replicates a transaction with correct journal entries', function () {
  344. $originalTransaction = Transaction::factory()
  345. ->forDefaultBankAccount()
  346. ->forUncategorizedRevenue()
  347. ->asDeposit(1000)
  348. ->create();
  349. livewire(Transactions::class)
  350. ->callTableAction(ReplicateAction::class, $originalTransaction);
  351. $replicatedTransaction = Transaction::whereKeyNot($originalTransaction->getKey())->first();
  352. expect($replicatedTransaction)->not->toBeNull();
  353. [$originalDebitAccount, $originalCreditAccount] = getTransactionDebitAndCreditAccounts($originalTransaction);
  354. [$replicatedDebitAccount, $replicatedCreditAccount] = getTransactionDebitAndCreditAccounts($replicatedTransaction);
  355. expect($replicatedTransaction)
  356. ->journalEntries->count()->toBe(2)
  357. ->journalEntries->sumDebits()->getValue()->toEqual(1000)
  358. ->journalEntries->sumCredits()->getValue()->toEqual(1000)
  359. ->description->toBe('(Copy of) ' . $originalTransaction->description)
  360. ->and($replicatedDebitAccount->name)->toBe($originalDebitAccount->name)
  361. ->and($replicatedCreditAccount->name)->toBe($originalCreditAccount->name);
  362. });
  363. it('bulk replicates transactions with correct journal entries', function () {
  364. $originalTransactions = Transaction::factory()
  365. ->forDefaultBankAccount()
  366. ->forUncategorizedRevenue()
  367. ->asDeposit(1000)
  368. ->count(3)
  369. ->create();
  370. livewire(Transactions::class)
  371. ->callTableBulkAction(ReplicateBulkAction::class, $originalTransactions);
  372. $replicatedTransactions = Transaction::whereKeyNot($originalTransactions->modelKeys())->get();
  373. expect($replicatedTransactions->count())->toBe(3);
  374. $originalTransactions->each(function (Transaction $originalTransaction) use ($replicatedTransactions) {
  375. /** @var Transaction $replicatedTransaction */
  376. $replicatedTransaction = $replicatedTransactions->firstWhere('description', '(Copy of) ' . $originalTransaction->description);
  377. expect($replicatedTransaction)->not->toBeNull();
  378. [$originalDebitAccount, $originalCreditAccount] = getTransactionDebitAndCreditAccounts($originalTransaction);
  379. [$replicatedDebitAccount, $replicatedCreditAccount] = getTransactionDebitAndCreditAccounts($replicatedTransaction);
  380. expect($replicatedTransaction)
  381. ->journalEntries->count()->toBe(2)
  382. ->journalEntries->sumDebits()->getValue()->toEqual(1000)
  383. ->journalEntries->sumCredits()->getValue()->toEqual(1000)
  384. ->and($replicatedDebitAccount->name)->toBe($originalDebitAccount->name)
  385. ->and($replicatedCreditAccount->name)->toBe($originalCreditAccount->name);
  386. });
  387. });
  388. it('can delete a transaction with journal entries', function () {
  389. $transaction = Transaction::factory()
  390. ->forDefaultBankAccount()
  391. ->forUncategorizedRevenue()
  392. ->asDeposit(1000)
  393. ->create();
  394. expect($transaction->journalEntries()->count())->toBe(2);
  395. livewire(Transactions::class)
  396. ->callTableAction(DeleteAction::class, $transaction);
  397. $this->assertModelMissing($transaction);
  398. $this->assertDatabaseEmpty('journal_entries');
  399. });
  400. it('can bulk delete transactions with journal entries', function () {
  401. $transactions = Transaction::factory()
  402. ->forDefaultBankAccount()
  403. ->forUncategorizedRevenue()
  404. ->asDeposit(1000)
  405. ->count(3)
  406. ->create();
  407. expect($transactions->count())->toBe(3);
  408. livewire(Transactions::class)
  409. ->callTableBulkAction(DeleteBulkAction::class, $transactions);
  410. $this->assertDatabaseEmpty('transactions');
  411. $this->assertDatabaseEmpty('journal_entries');
  412. });