From a0bd776abb207599de0970c85bdf7fd621f128d1 Mon Sep 17 00:00:00 2001 From: hackerESQ Date: Fri, 29 Aug 2025 15:47:18 -0500 Subject: [PATCH] fix: quantity validation should not count current transaction --- app/Http/Requests/TransactionRequest.php | 3 +- app/Rules/QuantityValidationRule.php | 8 +++-- database/factories/TransactionFactory.php | 6 ++-- .../manage-transaction-form.blade.php | 4 +-- tests/Api/TransactionsTest.php | 29 +++++++++++++++++++ tests/TransactionsTest.php | 19 ++++++++++++ 6 files changed, 61 insertions(+), 8 deletions(-) diff --git a/app/Http/Requests/TransactionRequest.php b/app/Http/Requests/TransactionRequest.php index 4eb7cdd..6fe135a 100644 --- a/app/Http/Requests/TransactionRequest.php +++ b/app/Http/Requests/TransactionRequest.php @@ -39,7 +39,8 @@ class TransactionRequest extends FormRequest $this->input('portfolio'), $this->requestOrModelValue('symbol', 'transaction'), $this->requestOrModelValue('transaction_type', 'transaction'), - $this->requestOrModelValue('date', 'transaction') + $this->requestOrModelValue('date', 'transaction'), + $this->transaction ), ], 'currency' => ['required', 'exists:currencies,currency'], diff --git a/app/Rules/QuantityValidationRule.php b/app/Rules/QuantityValidationRule.php index b0be87f..c831407 100644 --- a/app/Rules/QuantityValidationRule.php +++ b/app/Rules/QuantityValidationRule.php @@ -6,8 +6,8 @@ namespace App\Rules; use App\Models\Portfolio; use App\Models\Transaction; -use Illuminate\Support\Carbon; use Illuminate\Contracts\Validation\ValidationRule; +use Illuminate\Support\Carbon; class QuantityValidationRule implements ValidationRule { @@ -20,8 +20,9 @@ class QuantityValidationRule implements ValidationRule protected ?Portfolio $portfolio, protected ?string $symbol, protected ?string $transactionType, - protected string|Carbon|null $date - ) { } + protected string|Carbon|null $date, + protected ?Transaction $transaction + ) {} /** * Validate the attribute. @@ -42,6 +43,7 @@ class QuantityValidationRule implements ValidationRule ->sum('quantity'); $sales_qty = (float) $this->portfolio->transactions() + ->where('id', '!=', $this->transaction?->id) ->symbol($this->symbol) ->sell() ->whereDate('date', '<', $this->date) diff --git a/database/factories/TransactionFactory.php b/database/factories/TransactionFactory.php index 9a1ca8f..fcfa069 100644 --- a/database/factories/TransactionFactory.php +++ b/database/factories/TransactionFactory.php @@ -122,19 +122,21 @@ class TransactionFactory extends Factory ]); } - public function buy(): static + public function buy($quantity = 1): static { return $this->state(fn (array $attributes) => [ 'transaction_type' => 'BUY', + 'quantity' => $quantity, 'cost_basis' => $this->faker->randomFloat(2, 10, 500), 'sale_price' => null, ]); } - public function sell(): static + public function sell($quantity = 1): static { return $this->state(fn (array $attributes) => [ 'transaction_type' => 'SELL', + 'quantity' => $quantity, 'sale_price' => $this->faker->randomFloat(2, 10, 500), 'cost_basis' => null, ]); diff --git a/resources/views/transaction/manage-transaction-form.blade.php b/resources/views/transaction/manage-transaction-form.blade.php index cd1461b..2bbc15d 100644 --- a/resources/views/transaction/manage-transaction-form.blade.php +++ b/resources/views/transaction/manage-transaction-form.blade.php @@ -19,7 +19,7 @@ new class extends Component // props public ?Portfolio $portfolio; - public ?Transaction $transaction; + public ?Transaction $transaction = null; public ?string $portfolio_id; @@ -53,7 +53,7 @@ new class extends Component 'required', 'numeric', 'gt:0', - new QuantityValidationRule($this->portfolio, $this->symbol, $this->transaction_type, $this->date), + new QuantityValidationRule($this->portfolio, $this->symbol, $this->transaction_type, $this->date, $this->transaction), ], 'currency' => ['required', 'exists:currencies,currency'], 'cost_basis' => 'exclude_if:transaction_type,SELL|min:0|numeric', diff --git a/tests/Api/TransactionsTest.php b/tests/Api/TransactionsTest.php index c4b9e45..d1c7c90 100644 --- a/tests/Api/TransactionsTest.php +++ b/tests/Api/TransactionsTest.php @@ -114,6 +114,35 @@ class TransactionsTest extends TestCase ->assertJsonValidationErrors(['symbol']); } + public function test_cannot_sell_more_than_owned() + { + Artisan::call('db:seed', [ + '--class' => CurrencySeeder::class, + '--force' => true, + ]); + + $this->actingAs($this->user); + + $portfolio = Portfolio::factory()->create(); + + Transaction::factory(5)->buy()->lastYear()->portfolio($portfolio->id)->symbol('AAPL')->create(); + + $data = [ + 'symbol' => 'AAPL', + 'portfolio_id' => $this->portfolio->id, + 'transaction_type' => 'SELL', + 'quantity' => 6, + 'currency' => 'USD', + 'date' => now()->toDateString(), + 'sale_price' => 150, + ]; + + $this->actingAs($this->user) + ->postJson(route('api.transaction.store'), $data) + ->assertUnprocessable() + ->assertJsonValidationErrors(['quantity']); + } + public function test_can_show_a_transaction() { $this->actingAs($this->user); diff --git a/tests/TransactionsTest.php b/tests/TransactionsTest.php index 9cd6b8d..ba55da1 100644 --- a/tests/TransactionsTest.php +++ b/tests/TransactionsTest.php @@ -8,6 +8,7 @@ use App\Models\Holding; use App\Models\Portfolio; use App\Models\Transaction; use App\Models\User; +use App\Rules\QuantityValidationRule; use Illuminate\Foundation\Testing\RefreshDatabase; class TransactionsTest extends TestCase @@ -69,4 +70,22 @@ class TransactionsTest extends TestCase 0.01 ); } + + public function test_cannot_sell_more_than_owned(): void + { + $this->actingAs($user = User::factory()->create()); + + $portfolio = Portfolio::factory()->create(); + + Transaction::factory(5)->buy()->lastYear()->portfolio($portfolio->id)->symbol('AAPL')->create(); + $sale_transaction = Transaction::factory()->sell(6)->lastMonth()->portfolio($portfolio->id)->symbol('AAPL')->make(); + + $rule = new QuantityValidationRule($portfolio, $sale_transaction->symbol, 'SELL', $sale_transaction->date, $sale_transaction); + + $rule->validate('quantity', $sale_transaction->quantity, function () { + $this->assertFalse(false, 'Not permitted to sell more than owned.'); + }); + + $this->assertTrue(true); + } }