diff --git a/.devcontainer/devcontainer.example.json b/.devcontainer/devcontainer.example.json index 4babd74..4fa893c 100644 --- a/.devcontainer/devcontainer.example.json +++ b/.devcontainer/devcontainer.example.json @@ -9,11 +9,10 @@ "artdiniz.quitcontrol-vscode", "mikestead.dotenv", "ryu1kn.partial-diff", - "emallin.phpunit", + "calebporzio.better-phpunit", "eamodio.gitlens", "onecentlin.laravel-extension-pack", "onecentlin.php-productive-pack", - "beyondcode.tinkerwell" ], "remoteUser": "laradock", "shutdownAction": "none" diff --git a/.gitignore b/.gitignore index d8edc5f..096b057 100755 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ vendor coverage .phpunit.result.cache /.phpunit.cache -devcontainer.json \ No newline at end of file +devcontainer.json +/database/database.sqlite \ No newline at end of file diff --git a/composer.json b/composer.json index 7a4e618..79734e6 100755 --- a/composer.json +++ b/composer.json @@ -27,15 +27,14 @@ } ], "require": { - "php": "^8.1", - "illuminate/support": "^10.0" + "php": "^8.2" }, "require-dev": { - "doctrine/dbal": "^3.6", - "laravel/pint": "*", - "nunomaduro/larastan": "^2.5", - "orchestra/testbench": "^8.0", - "phpunit/phpunit": "^10.0" + "doctrine/dbal": "^4.0", + "laravel/pint": "1.16.1", + "larastan/larastan": "^2.9", + "orchestra/testbench": "^9.1.1", + "phpunit/phpunit": "^11.1" }, "autoload": { "psr-4": { diff --git a/database/database.sqlite.example b/database/database.sqlite.example new file mode 100644 index 0000000..e69de29 diff --git a/database/migrations/2019_01_25_000000_add_polymorphic_relation_to_transactions_table.php b/database/migrations/2019_01_25_000000_add_polymorphic_relation_to_transactions_table.php index b4ab4c1..2188871 100755 --- a/database/migrations/2019_01_25_000000_add_polymorphic_relation_to_transactions_table.php +++ b/database/migrations/2019_01_25_000000_add_polymorphic_relation_to_transactions_table.php @@ -21,7 +21,9 @@ public function up() ->after('wallet_id'); }); Schema::table($transactionTable, function (Blueprint $table) { - $table->unsignedInteger('reference_id')->nullable()->after('wallet_id'); + $table->unsignedInteger('reference_id') + ->nullable() + ->after('wallet_id'); }); } @@ -35,10 +37,10 @@ public function down() $transactionModelClass = config('wallet.transaction_model'); $transactionTable = (new $transactionModelClass())->getTable(); Schema::table($transactionTable, function (Blueprint $table) { - $table->dropColumn(['reference_type']); + $table->dropColumn('reference_type'); }); Schema::table($transactionTable, function (Blueprint $table) { - $table->dropColumn(['reference_id']); + $table->dropColumn('reference_id'); }); } } diff --git a/phpstan.neon b/phpstan.neon index 16a46ec..26765b8 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,5 @@ includes: - - ./vendor/nunomaduro/larastan/extension.neon + - ./vendor/larastan/larastan/extension.neon parameters: diff --git a/src/Facades/WalletFacade.php b/src/Facades/WalletFacade.php index 26afd28..f357b5c 100755 --- a/src/Facades/WalletFacade.php +++ b/src/Facades/WalletFacade.php @@ -5,7 +5,17 @@ use Illuminate\Support\Facades\Facade; /** + * + * @method static mixed config(string $relativePath, mixed $default) + * @method static array addingTransactionTypes() + * @method static array subtractingTransactionTypes() + * @method static array unbiasedTransactionTypes() + * @method static array biasedTransactionTypes() + * @method static array transactionTypes() + * @method static bool autoRecalculationActive() + * * @see \MannikJ\Laravel\Wallet\Models\Wallet + * @mixin \MannikJ\Laravel\Wallet\Models\Wallet */ class WalletFacade extends Facade { diff --git a/src/Models/Wallet.php b/src/Models/Wallet.php index 9d819f8..b0f9481 100755 --- a/src/Models/Wallet.php +++ b/src/Models/Wallet.php @@ -32,11 +32,11 @@ class Wallet extends Model implements ValidModelConstructor public function __construct(array $attributes = []) { $type = config('wallet.column_type'); - if ($type == 'decimal') { - $this->casts['balance'] = 'float'; - } elseif ($type == 'integer') { - $this->casts['balance'] = 'integer'; - } + + $this->casts['balance'] = $type === 'decimal' + ? 'float' + : 'integer'; + parent::__construct($attributes); } @@ -62,12 +62,12 @@ public function owner(): MorphTo * @param int $amount * @return MannikJ\Laravel\Wallet\Models\Transaction */ - public function deposit(int|float $amount, array $meta = [], string $type = 'deposit', bool $forceFail = false): Transaction + public function deposit(float $amount, array $meta = [], string $type = 'deposit', bool $forceFail = false): Transaction { $accepted = $amount >= 0 - && ! $forceFail ? true : false; + && !$forceFail ? true : false; - if (! $this->exists) { + if (!$this->exists) { $this->save(); } @@ -79,7 +79,7 @@ public function deposit(int|float $amount, array $meta = [], string $type = 'dep 'deleted_at' => $accepted ? null : now(), ]); - if (! $accepted && ! $forceFail) { + if (!$accepted && !$forceFail) { throw new UnacceptedTransactionException($transaction, 'Deposit not accepted!'); } @@ -91,10 +91,10 @@ public function deposit(int|float $amount, array $meta = [], string $type = 'dep /** * Fail to move credits to this account. * - * @param int $amount + * @param float $amount * @return MannikJ\Laravel\Wallet\Models\Transaction */ - public function failDeposit(int|float $amount, array $meta = [], string $type = 'deposit'): Transaction + public function failDeposit(float $amount, array $meta = [], string $type = 'deposit'): Transaction { return $this->deposit($amount, $meta, $type, true); } @@ -102,16 +102,16 @@ public function failDeposit(int|float $amount, array $meta = [], string $type = /** * Attempt to move credits from this account. * - * @param int $amount Only the absolute value will be considered + * @param float $amount Only the absolute value will be considered * @return MannikJ\Laravel\Wallet\Models\Transaction */ - public function withdraw(int|float $amount, array $meta = [], string $type = 'withdraw', bool $guarded = true) + public function withdraw(float $amount, array $meta = [], string $type = 'withdraw', bool $guarded = true) { $accepted = $guarded ? $this->canWithdraw($amount) : true; - if (! $this->exists) { + if (!$this->exists) { $this->save(); } @@ -123,7 +123,7 @@ public function withdraw(int|float $amount, array $meta = [], string $type = 'wi 'deleted_at' => $accepted ? null : now(), ]); - if (! $accepted) { + if (!$accepted) { throw new UnacceptedTransactionException($transaction, 'Withdrawal not accepted due to insufficient funds!'); } @@ -135,7 +135,7 @@ public function withdraw(int|float $amount, array $meta = [], string $type = 'wi /** * Move credits from this account. * - * @param int $amount + * @param float $amount */ public function forceWithdraw(int|float $amount, array $meta = [], string $type = 'withdraw') { @@ -145,28 +145,37 @@ public function forceWithdraw(int|float $amount, array $meta = [], string $type /** * Determine if the user can withdraw the given amount. * - * @param int $amount + * @param float $amount * @return bool */ - public function canWithdraw(int|float $amount = null) + public function canWithdraw(float $amount = null) { - return $amount ? $this->balance >= abs($amount) : $this->balance > 0; + return $amount + ? $this->balance >= abs($amount) + : $this->balance > 0; } /** * Set wallet balance to desired value. * Will automatically create the necessary transaction. * - * @param int $balance + * @param float $balance + * @param string $comment + * @return MannikJ\Laravel\Wallet\Models\Transaction */ - public function setBalance(int|float $amount, string $comment = 'Manual offset transaction') + public function setBalance(float $amount, string $comment = 'Manual offset transaction') { $actualBalance = $this->actualBalance(); $difference = $amount - $actualBalance; + if ($difference == 0) { return; } - $type = $difference > 0 ? 'deposit' : 'forceWithdraw'; + + $type = $difference > 0 + ? 'deposit' + : 'forceWithdraw'; + $this->balance = $actualBalance; $this->save(); @@ -177,20 +186,23 @@ public function setBalance(int|float $amount, string $comment = 'Manual offset t * Returns the actual balance for this wallet. * Might be different from the balance property if the database is manipulated. * + * @param bool $save * @return float balance */ - public function actualBalance(bool $save = false) + public function actualBalance(bool $save = false): float { $undefined = $this->transactions() ->whereNotIn('type', WalletFacade::biasedTransactionTypes()) ->sum('amount'); + $credits = $this->transactions() ->whereIn('type', WalletFacade::addingTransactionTypes()) - ->sum(DB::raw('abs(amount)')); + ->sum(DB::raw('ABS(amount)')); $debits = $this->transactions() ->whereIn('type', WalletFacade::subtractingTransactionTypes()) - ->sum(DB::raw('abs(amount)')); + ->sum(DB::raw('ABS(amount)')); + $balance = $undefined + $credits - $debits; if ($save) { diff --git a/src/Services/Wallet.php b/src/Services/Wallet.php index 0a06881..cf2fc4e 100755 --- a/src/Services/Wallet.php +++ b/src/Services/Wallet.php @@ -4,6 +4,17 @@ class Wallet { + public function config($relativePath, $default) + { + $path = "wallet"; + + if ($relativePath) { + $path .= ".$relativePath"; + } + + return config($path, $default); + } + public function addingTransactionTypes(): array { return config('wallet.adding_transaction_types', []); diff --git a/tests/unit/HasWalletTest.php b/tests/unit/HasWalletTest.php index ddcda1e..05c7bdf 100755 --- a/tests/unit/HasWalletTest.php +++ b/tests/unit/HasWalletTest.php @@ -2,16 +2,20 @@ namespace MannikJ\Laravel\Wallet\Tests\Unit; +use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Collection; use MannikJ\Laravel\Wallet\Models\Wallet; use MannikJ\Laravel\Wallet\Tests\Factories\TransactionFactory; use MannikJ\Laravel\Wallet\Tests\Factories\UserFactory; use MannikJ\Laravel\Wallet\Tests\Factories\WalletFactory; use MannikJ\Laravel\Wallet\Tests\TestCase; +use PHPUnit\Framework\Attributes\Test; class HasWalletTest extends TestCase { - /** @test */ + use RefreshDatabase; + + #[Test] public function wallet() { $user = UserFactory::new()->create(); @@ -20,7 +24,7 @@ public function wallet() $this->assertTrue($user->wallet->balance === 0.0); } - /** @test */ + #[Test] public function wallet_transactions() { $user1 = UserFactory::new()->create(); diff --git a/tests/unit/RecalculateWalletBalanceTest.php b/tests/unit/RecalculateWalletBalanceTest.php index 54e8c6a..feeaace 100755 --- a/tests/unit/RecalculateWalletBalanceTest.php +++ b/tests/unit/RecalculateWalletBalanceTest.php @@ -2,14 +2,18 @@ namespace MannikJ\Laravel\Wallet\Tests\Unit; +use Illuminate\Foundation\Testing\RefreshDatabase; use MannikJ\Laravel\Wallet\Jobs\RecalculateWalletBalance; use MannikJ\Laravel\Wallet\Models\Transaction; use MannikJ\Laravel\Wallet\Tests\Factories\WalletFactory; use MannikJ\Laravel\Wallet\Tests\TestCase; +use PHPUnit\Framework\Attributes\Test; class RecalculateWalletBalanceTest extends TestCase { - /** @test */ + use RefreshDatabase; + + #[Test] public function dispatch() { config(['wallet.auto_recalculate_balance' => true]); diff --git a/tests/unit/TransactionTest.php b/tests/unit/TransactionTest.php index 4f6266e..74d7829 100755 --- a/tests/unit/TransactionTest.php +++ b/tests/unit/TransactionTest.php @@ -2,23 +2,27 @@ namespace MannikJ\Laravel\Wallet\Tests\Unit; +use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Collection; use MannikJ\Laravel\Wallet\Models\Transaction; use MannikJ\Laravel\Wallet\Models\Wallet; use MannikJ\Laravel\Wallet\Tests\Factories\TransactionFactory; use MannikJ\Laravel\Wallet\Tests\Factories\WalletFactory; use MannikJ\Laravel\Wallet\Tests\TestCase; +use PHPUnit\Framework\Attributes\Test; class TransactionTest extends TestCase { - /** @test */ + use RefreshDatabase; + + #[Test] public function wallet() { $transaction = TransactionFactory::new()->create(); $this->assertInstanceOf(Wallet::class, $transaction->wallet); } - /** @test */ + #[Test] public function origin() { $origin = TransactionFactory::new()->create(); @@ -30,7 +34,7 @@ public function origin() $this->assertTrue($origin->is($transaction->origin)); } - /** @test */ + #[Test] public function children() { $origin = TransactionFactory::new()->create(); @@ -42,7 +46,7 @@ public function children() $this->assertTrue($origin->is($transaction->origin)); } - /** @test */ + #[Test] public function reference() { $transaction = TransactionFactory::new()->create(); @@ -51,7 +55,7 @@ public function reference() $this->assertTrue($transaction->wallet->is($transaction->reference)); } - /** @test */ + #[Test] public function update() { $transaction = TransactionFactory::new()->create(['amount' => 20, 'type' => 'deposit']); @@ -66,7 +70,7 @@ public function update() $this->assertEquals(-20, $transaction->wallet->refresh()->balance); } - /** @test */ + #[Test] public function create_converts_amount_to_absolute_value() { $wallet = WalletFactory::new()->create(); @@ -74,7 +78,7 @@ public function create_converts_amount_to_absolute_value() $this->assertEquals(20, $transaction->getAttributes()['amount']); } - /** @test */ + #[Test] public function delete_model() { $transaction = TransactionFactory::new()->create([ @@ -92,7 +96,7 @@ public function delete_model() $this->assertEquals(-20, $transaction->wallet->refresh()->balance); } - /** @test */ + #[Test] public function replace() { $timestamp = now()->subHours(1); @@ -112,7 +116,7 @@ public function replace() $this->assertTrue($replacement->origin->trashed()); } - /** @test */ + #[Test] public function generated_hash_is_set() { $transaction = TransactionFactory::new()->create(); @@ -123,7 +127,7 @@ public function generated_hash_is_set() }); } - /** @test */ + #[Test] public function get_total_amount() { $transaction = TransactionFactory::new()->deposit()->create(['amount' => '5']); @@ -149,7 +153,7 @@ public function get_total_amount() $this->assertEquals($price, $transaction->getTotalAmount()); } - /** @test */ + #[Test] public function scope_select_total_amount() { $transaction = TransactionFactory::new()->deposit()->create(['amount' => '5']); @@ -173,7 +177,7 @@ public function scope_select_total_amount() $this->assertEquals($price, $transaction->where('id', $transaction->id)->selectTotalAmount()->first()->getAttributes()['total_amount']); } - /** @test */ + #[Test] public function get_total_amount_attribute() { $transaction = TransactionFactory::new()->deposit()->create(['amount' => '5']); diff --git a/tests/unit/WalletTest.php b/tests/unit/WalletTest.php index 81e960f..9271f73 100755 --- a/tests/unit/WalletTest.php +++ b/tests/unit/WalletTest.php @@ -2,6 +2,7 @@ namespace MannikJ\Laravel\Wallet\Tests\Unit; +use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Queue; use MannikJ\Laravel\Wallet\Exceptions\UnacceptedTransactionException; @@ -13,17 +14,20 @@ use MannikJ\Laravel\Wallet\Tests\Factories\WalletFactory; use MannikJ\Laravel\Wallet\Tests\Models\User; use MannikJ\Laravel\Wallet\Tests\TestCase; +use PHPUnit\Framework\Attributes\Test; class WalletTest extends TestCase { - /** @test */ + use RefreshDatabase; + + #[Test] public function owner() { $wallet = WalletFactory::new()->create(); $this->assertInstanceOf(User::class, $wallet->owner); } - /** @test */ + #[Test] public function delete_and_restore_wallet() { $user = UserFactory::new()->create(); @@ -46,7 +50,7 @@ public function delete_and_restore_wallet() $this->assertEquals(2, Transaction::count()); } - /** @test */ + #[Test] public function deposit() { $user = UserFactory::new()->create(); @@ -67,7 +71,7 @@ public function deposit() $this->assertEquals(-25, $user->wallet->balance); } - /** @test */ + #[Test] public function deposit_negative_amount() { $user = UserFactory::new()->create(); @@ -78,7 +82,7 @@ public function deposit_negative_amount() $transaction = $user->wallet->deposit(-30); } - /** @test */ + #[Test] public function fail_deposit() { $user = UserFactory::new()->create(); @@ -94,7 +98,7 @@ public function fail_deposit() $this->assertEquals(10000, $user->wallet->fresh()->balance); } - /** @test */ + #[Test] public function force_withdraw() { $user = UserFactory::new()->create(); @@ -107,7 +111,7 @@ public function force_withdraw() $this->assertEquals(-10000, $user->fresh()->balance); } - /** @test */ + #[Test] public function can_withdraw() { $user = UserFactory::new()->create(); @@ -124,7 +128,7 @@ public function can_withdraw() $this->assertFalse($user->wallet->canWithdraw(-6)); } - /** @test */ + #[Test] public function withdraw() { $user = UserFactory::new()->create(); @@ -139,7 +143,7 @@ public function withdraw() $this->assertEquals(2, $user->wallet->walletTransactions()->withTrashed()->count()); } - /** @test */ + #[Test] public function set_balance() { $user = UserFactory::new()->create(); @@ -163,7 +167,7 @@ public function set_balance() $this->assertEquals('Manual offset transaction', $offsetTransaction->meta['comment']); } - /** @test */ + #[Test] public function actual_balance() { $wallet = WalletFactory::new()->create(); @@ -185,7 +189,7 @@ public function actual_balance() $this->assertEquals($expectedBalance, $user->wallet->actualBalance()); } - /** @test */ + #[Test] public function balance_change_doesnt_trigger_recalculation() { Queue::fake(); @@ -195,7 +199,7 @@ public function balance_change_doesnt_trigger_recalculation() Queue::assertNotPushed(RecalculateWalletBalance::class); } - /** @test */ + #[Test] public function balance_change_triggers_recalculation_if_activated() { Queue::fake(); @@ -206,7 +210,7 @@ public function balance_change_triggers_recalculation_if_activated() Queue::assertPushed(RecalculateWalletBalance::class); } - /** @test */ + #[Test] public function recalculation_performance() { $user = UserFactory::new()->create();