Skip to content

Commit a0bd776

Browse files
committed
fix: quantity validation should not count current transaction
1 parent afcafa6 commit a0bd776

6 files changed

Lines changed: 61 additions & 8 deletions

File tree

app/Http/Requests/TransactionRequest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ public function rules(): array
3939
$this->input('portfolio'),
4040
$this->requestOrModelValue('symbol', 'transaction'),
4141
$this->requestOrModelValue('transaction_type', 'transaction'),
42-
$this->requestOrModelValue('date', 'transaction')
42+
$this->requestOrModelValue('date', 'transaction'),
43+
$this->transaction
4344
),
4445
],
4546
'currency' => ['required', 'exists:currencies,currency'],

app/Rules/QuantityValidationRule.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66

77
use App\Models\Portfolio;
88
use App\Models\Transaction;
9-
use Illuminate\Support\Carbon;
109
use Illuminate\Contracts\Validation\ValidationRule;
10+
use Illuminate\Support\Carbon;
1111

1212
class QuantityValidationRule implements ValidationRule
1313
{
@@ -20,8 +20,9 @@ public function __construct(
2020
protected ?Portfolio $portfolio,
2121
protected ?string $symbol,
2222
protected ?string $transactionType,
23-
protected string|Carbon|null $date
24-
) { }
23+
protected string|Carbon|null $date,
24+
protected ?Transaction $transaction
25+
) {}
2526

2627
/**
2728
* Validate the attribute.
@@ -42,6 +43,7 @@ public function validate(string $attribute, mixed $value, \Closure $fail): void
4243
->sum('quantity');
4344

4445
$sales_qty = (float) $this->portfolio->transactions()
46+
->where('id', '!=', $this->transaction?->id)
4547
->symbol($this->symbol)
4648
->sell()
4749
->whereDate('date', '<', $this->date)

database/factories/TransactionFactory.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,19 +122,21 @@ public function salePrice($sale_price): static
122122
]);
123123
}
124124

125-
public function buy(): static
125+
public function buy($quantity = 1): static
126126
{
127127
return $this->state(fn (array $attributes) => [
128128
'transaction_type' => 'BUY',
129+
'quantity' => $quantity,
129130
'cost_basis' => $this->faker->randomFloat(2, 10, 500),
130131
'sale_price' => null,
131132
]);
132133
}
133134

134-
public function sell(): static
135+
public function sell($quantity = 1): static
135136
{
136137
return $this->state(fn (array $attributes) => [
137138
'transaction_type' => 'SELL',
139+
'quantity' => $quantity,
138140
'sale_price' => $this->faker->randomFloat(2, 10, 500),
139141
'cost_basis' => null,
140142
]);

resources/views/transaction/manage-transaction-form.blade.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
// props
2020
public ?Portfolio $portfolio;
2121
22-
public ?Transaction $transaction;
22+
public ?Transaction $transaction = null;
2323
2424
public ?string $portfolio_id;
2525
@@ -53,7 +53,7 @@ public function rules()
5353
'required',
5454
'numeric',
5555
'gt:0',
56-
new QuantityValidationRule($this->portfolio, $this->symbol, $this->transaction_type, $this->date),
56+
new QuantityValidationRule($this->portfolio, $this->symbol, $this->transaction_type, $this->date, $this->transaction),
5757
],
5858
'currency' => ['required', 'exists:currencies,currency'],
5959
'cost_basis' => 'exclude_if:transaction_type,SELL|min:0|numeric',

tests/Api/TransactionsTest.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,35 @@ public function test_cannot_create_transaction_without_required_fields()
114114
->assertJsonValidationErrors(['symbol']);
115115
}
116116

117+
public function test_cannot_sell_more_than_owned()
118+
{
119+
Artisan::call('db:seed', [
120+
'--class' => CurrencySeeder::class,
121+
'--force' => true,
122+
]);
123+
124+
$this->actingAs($this->user);
125+
126+
$portfolio = Portfolio::factory()->create();
127+
128+
Transaction::factory(5)->buy()->lastYear()->portfolio($portfolio->id)->symbol('AAPL')->create();
129+
130+
$data = [
131+
'symbol' => 'AAPL',
132+
'portfolio_id' => $this->portfolio->id,
133+
'transaction_type' => 'SELL',
134+
'quantity' => 6,
135+
'currency' => 'USD',
136+
'date' => now()->toDateString(),
137+
'sale_price' => 150,
138+
];
139+
140+
$this->actingAs($this->user)
141+
->postJson(route('api.transaction.store'), $data)
142+
->assertUnprocessable()
143+
->assertJsonValidationErrors(['quantity']);
144+
}
145+
117146
public function test_can_show_a_transaction()
118147
{
119148
$this->actingAs($this->user);

tests/TransactionsTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use App\Models\Portfolio;
99
use App\Models\Transaction;
1010
use App\Models\User;
11+
use App\Rules\QuantityValidationRule;
1112
use Illuminate\Foundation\Testing\RefreshDatabase;
1213

1314
class TransactionsTest extends TestCase
@@ -69,4 +70,22 @@ public function test_transaction_synced_to_holding(): void
6970
0.01
7071
);
7172
}
73+
74+
public function test_cannot_sell_more_than_owned(): void
75+
{
76+
$this->actingAs($user = User::factory()->create());
77+
78+
$portfolio = Portfolio::factory()->create();
79+
80+
Transaction::factory(5)->buy()->lastYear()->portfolio($portfolio->id)->symbol('AAPL')->create();
81+
$sale_transaction = Transaction::factory()->sell(6)->lastMonth()->portfolio($portfolio->id)->symbol('AAPL')->make();
82+
83+
$rule = new QuantityValidationRule($portfolio, $sale_transaction->symbol, 'SELL', $sale_transaction->date, $sale_transaction);
84+
85+
$rule->validate('quantity', $sale_transaction->quantity, function () {
86+
$this->assertFalse(false, 'Not permitted to sell more than owned.');
87+
});
88+
89+
$this->assertTrue(true);
90+
}
7291
}

0 commit comments

Comments
 (0)