diff --git a/phpstan.src.baseline.neon b/phpstan.src.baseline.neon index ce5b7ad55..e574d4ab3 100644 --- a/phpstan.src.baseline.neon +++ b/phpstan.src.baseline.neon @@ -196,7 +196,7 @@ parameters: path: src/Models/Transaction.php - - message: "#^Property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$table \\(string\\) does not accept mixed\\.$#" + message: "#^Property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$table \\(string\\|null\\) does not accept mixed\\.$#" count: 1 path: src/Models/Transaction.php @@ -231,7 +231,7 @@ parameters: path: src/Models/Transfer.php - - message: "#^Property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$table \\(string\\) does not accept mixed\\.$#" + message: "#^Property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$table \\(string\\|null\\) does not accept mixed\\.$#" count: 1 path: src/Models/Transfer.php @@ -271,7 +271,7 @@ parameters: path: src/Models/Wallet.php - - message: "#^Property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$table \\(string\\) does not accept mixed\\.$#" + message: "#^Property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$table \\(string\\|null\\) does not accept mixed\\.$#" count: 1 path: src/Models/Wallet.php diff --git a/src/Traits/CanConfirm.php b/src/Traits/CanConfirm.php index f21d6e3af..0946239a0 100644 --- a/src/Traits/CanConfirm.php +++ b/src/Traits/CanConfirm.php @@ -45,19 +45,33 @@ trait CanConfirm */ public function confirm(Transaction $transaction): bool { - // Check if the wallet has enough money + // Execute the confirmation process within an atomic block to ensure data consistency. return app(AtomicServiceInterface::class)->block($this, function () use ($transaction): bool { + // Check if the transaction is already confirmed. + // If it is, throw an exception. + if ($transaction->confirmed) { + // Why is there a check here without calling refresh? + // It's because this check can be performed in force confirm again. + throw new ConfirmedInvalid( + // Get the error message from the translator service. + app(TranslatorServiceInterface::class)->get('wallet::errors.confirmed_invalid'), + // Set the error code to CONFIRMED_INVALID. + ExceptionInterface::CONFIRMED_INVALID + ); + } + + // Check if the transaction type is withdrawal. if ($transaction->type === Transaction::TYPE_WITHDRAW) { - // Check if the wallet has enough money + // Check if the wallet has enough money to cover the withdrawal amount. app(ConsistencyServiceInterface::class)->checkPotential( - // Get the wallet + // Get the wallet. app(CastServiceInterface::class)->getWallet($this), - // Negative amount + // Negate the withdrawal amount to check for sufficient funds. app(MathServiceInterface::class)->negative($transaction->amount) ); } - // Force confirm the transaction + // Force confirm the transaction. return $this->forceConfirm($transaction); }); } @@ -118,7 +132,7 @@ public function resetConfirm(Transaction $transaction): bool // Reset the confirmation of the transaction in a single database transaction return app(AtomicServiceInterface::class)->block($this, function () use ($transaction) { // Check if the transaction is already confirmed - if (! $transaction->confirmed) { + if (! $transaction->refresh()->confirmed) { throw new UnconfirmedInvalid( // If the transaction is not confirmed, throw an `UnconfirmedInvalid` exception app(TranslatorServiceInterface::class)->get('wallet::errors.unconfirmed_invalid'), @@ -195,7 +209,7 @@ public function forceConfirm(Transaction $transaction): bool // Attempt to confirm the transaction in a single database transaction return app(AtomicServiceInterface::class)->block($this, function () use ($transaction) { // Check if the transaction is already confirmed - if ($transaction->confirmed) { + if ($transaction->refresh()->confirmed) { throw new ConfirmedInvalid( app(TranslatorServiceInterface::class)->get('wallet::errors.confirmed_invalid'), ExceptionInterface::CONFIRMED_INVALID diff --git a/tests/Units/Domain/ConfirmTest.php b/tests/Units/Domain/ConfirmTest.php index 9533ec53a..834d59546 100644 --- a/tests/Units/Domain/ConfirmTest.php +++ b/tests/Units/Domain/ConfirmTest.php @@ -67,6 +67,27 @@ public function testSafe(): void self::assertFalse($transaction->confirmed); } + public function testSafeConfirmedInvalid(): void + { + /** @var Buyer $buyer */ + $buyer = BuyerFactory::new()->create(); + $wallet = $buyer->wallet; + + self::assertSame(0, $wallet->balanceInt); + + $transaction = $wallet->forceWithdraw(1000, [ + 'desc' => 'confirmed', + ]); + + self::assertSame(-1000, $wallet->balanceInt); + self::assertTrue($transaction->confirmed); + self::assertTrue($transaction->getKey() > 0); + + self::assertTrue($wallet->safeConfirm($transaction)); + self::assertSame(-1000, $wallet->balanceInt); + self::assertTrue($transaction->confirmed); + } + public function testSafeResetConfirm(): void { /** @var Buyer $buyer */ @@ -127,6 +148,25 @@ public function testForce(): void self::assertTrue($transaction->confirmed); } + public function testForceConfirmedInvalid(): void + { + $this->expectException(ConfirmedInvalid::class); + $this->expectExceptionCode(ExceptionInterface::CONFIRMED_INVALID); + $this->expectExceptionMessageStrict(trans('wallet::errors.confirmed_invalid')); + + /** @var Buyer $buyer */ + $buyer = BuyerFactory::new()->create(); + $wallet = $buyer->wallet; + + self::assertSame(0, $wallet->balanceInt); + + $transaction = $wallet->forceWithdraw(1000); + self::assertSame(-1000, $wallet->balanceInt); + self::assertTrue($transaction->confirmed); + + $wallet->forceConfirm($transaction); + } + public function testUnconfirmed(): void { /** @var Buyer $buyer */ @@ -198,6 +238,31 @@ public function testSafeUnconfirmed(): void self::assertTrue($wallet->safeResetConfirm($transaction)); } + public function testSafeUnconfirmedWalletOwnerInvalid(): void + { + /** + * @var Buyer $buyer1 + * @var Buyer $buyer2 + **/ + [$buyer1, $buyer2] = BuyerFactory::times(2)->create(); + $wallet1 = $buyer1->wallet; + $wallet2 = $buyer2->wallet; + + self::assertTrue($wallet1->saveOrFail()); + self::assertTrue($wallet2->saveOrFail()); + + self::assertSame(0, $wallet1->balanceInt); + self::assertSame(0, $wallet2->balanceInt); + + $transaction1 = $wallet1->deposit(1000, null, true); + self::assertSame(1000, $wallet1->balanceInt); + self::assertTrue($transaction1->confirmed); + + self::assertFalse($wallet2->safeResetConfirm($transaction1)); + self::assertSame(1000, $wallet1->balanceInt); + self::assertTrue($transaction1->confirmed); + } + public function testWalletOwnerInvalid(): void { $this->expectException(WalletOwnerInvalid::class); @@ -223,6 +288,31 @@ public function testWalletOwnerInvalid(): void $secondWallet->confirm($transaction); } + public function testForceWalletOwnerInvalid(): void + { + $this->expectException(WalletOwnerInvalid::class); + $this->expectExceptionCode(ExceptionInterface::WALLET_OWNER_INVALID); + $this->expectExceptionMessageStrict(trans('wallet::errors.owner_invalid')); + + /** + * @var Buyer $first + * @var Buyer $second + */ + [$first, $second] = BuyerFactory::times(2)->create(); + $firstWallet = $first->wallet; + $secondWallet = $second->wallet; + + self::assertSame(0, $firstWallet->balanceInt); + + $transaction = $firstWallet->deposit(1000, [ + 'desc' => 'unconfirmed', + ], false); + self::assertSame(0, $firstWallet->balanceInt); + self::assertFalse($transaction->confirmed); + + $secondWallet->forceConfirm($transaction); + } + public function testUserConfirm(): void { /** @var UserConfirm $userConfirm */ diff --git a/tests/Units/Domain/EagerLoadingTest.php b/tests/Units/Domain/EagerLoadingTest.php index 89d11d910..172d4d2fb 100644 --- a/tests/Units/Domain/EagerLoadingTest.php +++ b/tests/Units/Domain/EagerLoadingTest.php @@ -33,7 +33,7 @@ public function testUuidDuplicate(): void } /** @var Collection $buyers */ - $buyers = Buyer::with('wallet') + $buyers = Buyer::with('wallet.walletTransactions') ->whereIn('id', $buyerTimes->pluck('id')->toArray()) ->paginate(10); @@ -42,6 +42,7 @@ public function testUuidDuplicate(): void foreach ($buyers as $buyer) { self::assertTrue($buyer->relationLoaded('wallet')); self::assertTrue($buyer->wallet->relationLoaded('holder')); + self::assertTrue($buyer->wallet->relationLoaded('walletTransactions')); $uuids[] = $buyer->wallet->uuid; $balances[] = $buyer->wallet->balanceInt; @@ -85,8 +86,14 @@ public function testMultiWallets(): void ]); /** @var UserMulti $user */ - $user = UserMulti::with('wallets')->find($multi->getKey()); + $user = UserMulti::with('wallets.walletTransactions')->find($multi->getKey()); self::assertTrue($user->relationLoaded('wallets')); + self::assertNotEmpty($user->wallets); + + foreach ($user->wallets as $wallet) { + self::assertTrue($wallet->relationLoaded('walletTransactions')); + } + self::assertNotNull($user->getWallet('hello')); self::assertNotNull($user->getWallet('world')); self::assertTrue($user->getWallet('hello')->relationLoaded('holder'));