Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 921ac28ba7 | |||
| 2b099b3156 | |||
| ee7668df6f |
@@ -21,9 +21,9 @@ class HoldingController extends Controller
|
||||
$query->where('transactions.symbol', $symbol);
|
||||
},
|
||||
])
|
||||
->symbol($symbol)
|
||||
->portfolio($portfolio->id)
|
||||
->firstOrFail();
|
||||
->symbol($symbol)
|
||||
->portfolio($portfolio->id)
|
||||
->firstOrFail();
|
||||
|
||||
$formattedTransactions = $holding->getFormattedTransactions();
|
||||
|
||||
|
||||
@@ -39,8 +39,7 @@ class TransactionRequest extends FormRequest
|
||||
$this->input('portfolio'),
|
||||
$this->requestOrModelValue('symbol', 'transaction'),
|
||||
$this->requestOrModelValue('transaction_type', 'transaction'),
|
||||
$this->requestOrModelValue('date', 'transaction'),
|
||||
$this->transaction
|
||||
$this->requestOrModelValue('date', 'transaction')
|
||||
),
|
||||
],
|
||||
'currency' => ['required', 'exists:currencies,currency'],
|
||||
|
||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
||||
namespace App\Rules;
|
||||
|
||||
use App\Models\Portfolio;
|
||||
use App\Models\Transaction;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
@@ -20,8 +19,7 @@ class QuantityValidationRule implements ValidationRule
|
||||
protected ?Portfolio $portfolio,
|
||||
protected ?string $symbol,
|
||||
protected ?string $transactionType,
|
||||
protected string|Carbon|null $date,
|
||||
protected ?Transaction $transaction
|
||||
protected string|Carbon|null $date
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -43,7 +41,6 @@ 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)
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@
|
||||
"laravel/sail": "^1.26",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.0",
|
||||
"phpunit/phpunit": "^11.0"
|
||||
"phpunit/phpunit": "^11.0.1"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
|
||||
Generated
+31
-31
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "09faf36704392ba5099e39fe62908057",
|
||||
"content-hash": "d5d786656eb888c2966c648ddf946280",
|
||||
"packages": [
|
||||
{
|
||||
"name": "aws/aws-crt-php",
|
||||
@@ -62,16 +62,16 @@
|
||||
},
|
||||
{
|
||||
"name": "aws/aws-sdk-php",
|
||||
"version": "3.356.7",
|
||||
"version": "3.356.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/aws/aws-sdk-php.git",
|
||||
"reference": "6b44237a218485bf43a0015600aebf43cb726d4e"
|
||||
"reference": "5872ccb5100c4afb0dae3db0bd46636f63ae8147"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/6b44237a218485bf43a0015600aebf43cb726d4e",
|
||||
"reference": "6b44237a218485bf43a0015600aebf43cb726d4e",
|
||||
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/5872ccb5100c4afb0dae3db0bd46636f63ae8147",
|
||||
"reference": "5872ccb5100c4afb0dae3db0bd46636f63ae8147",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -153,9 +153,9 @@
|
||||
"support": {
|
||||
"forum": "https://github.com/aws/aws-sdk-php/discussions",
|
||||
"issues": "https://github.com/aws/aws-sdk-php/issues",
|
||||
"source": "https://github.com/aws/aws-sdk-php/tree/3.356.7"
|
||||
"source": "https://github.com/aws/aws-sdk-php/tree/3.356.5"
|
||||
},
|
||||
"time": "2025-08-28T18:14:39+00:00"
|
||||
"time": "2025-08-26T18:05:04+00:00"
|
||||
},
|
||||
{
|
||||
"name": "bacon/bacon-qr-code",
|
||||
@@ -1803,21 +1803,21 @@
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/hackeresq/filter-models",
|
||||
"reference": "e92c1e1e8af299cb2c3c3e6d7768f2fb2dcb9146"
|
||||
"reference": "847950d3277fe7df3a2dcdcdd3ba37d3b07ee667"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/hackeresq/filter-models/zipball/e92c1e1e8af299cb2c3c3e6d7768f2fb2dcb9146",
|
||||
"reference": "e92c1e1e8af299cb2c3c3e6d7768f2fb2dcb9146",
|
||||
"url": "https://api.github.com/repos/hackeresq/filter-models/zipball/847950d3277fe7df3a2dcdcdd3ba37d3b07ee667",
|
||||
"reference": "847950d3277fe7df3a2dcdcdd3ba37d3b07ee667",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"laravel/framework": "^11.0||^12.0",
|
||||
"laravel/framework": "^11.9",
|
||||
"php": "^8.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"orchestra/testbench": "^9.9",
|
||||
"phpunit/phpunit": "^10.0||^11.0"
|
||||
"phpunit/phpunit": "^10.0|^11.0"
|
||||
},
|
||||
"default-branch": true,
|
||||
"type": "library",
|
||||
@@ -1839,7 +1839,7 @@
|
||||
}
|
||||
},
|
||||
"description": "Simple package to filter your Laravel models with query parameters",
|
||||
"time": "2025-08-29T02:40:27+00:00"
|
||||
"time": "2025-01-27T23:18:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "investbrainapp/frankfurter-client",
|
||||
@@ -1847,16 +1847,16 @@
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/investbrainapp/frankfurter-client",
|
||||
"reference": "d2a96d7db2d17e91245b0cf2146e2b8a295b8d4b"
|
||||
"reference": "738b2b53f48b7cdf4d66c44a592430dea4de9fd0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/investbrainapp/frankfurter-client/zipball/d2a96d7db2d17e91245b0cf2146e2b8a295b8d4b",
|
||||
"reference": "d2a96d7db2d17e91245b0cf2146e2b8a295b8d4b",
|
||||
"url": "https://api.github.com/repos/investbrainapp/frankfurter-client/zipball/738b2b53f48b7cdf4d66c44a592430dea4de9fd0",
|
||||
"reference": "738b2b53f48b7cdf4d66c44a592430dea4de9fd0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"laravel/framework": "^11.0||^12.0",
|
||||
"laravel/framework": "^11.9",
|
||||
"php": "^8.2"
|
||||
},
|
||||
"default-branch": true,
|
||||
@@ -1874,7 +1874,7 @@
|
||||
}
|
||||
},
|
||||
"description": "Laravel SDK for interacting with the Frankfurter currency exchange API",
|
||||
"time": "2025-08-29T02:39:41+00:00"
|
||||
"time": "2025-04-11T02:35:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "jfcherng/php-color-output",
|
||||
@@ -9911,16 +9911,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
"version": "11.0.11",
|
||||
"version": "11.0.10",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
|
||||
"reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4"
|
||||
"reference": "1a800a7446add2d79cc6b3c01c45381810367d76"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4",
|
||||
"reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/1a800a7446add2d79cc6b3c01c45381810367d76",
|
||||
"reference": "1a800a7446add2d79cc6b3c01c45381810367d76",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -9977,7 +9977,7 @@
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
|
||||
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.11"
|
||||
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/show"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -9997,7 +9997,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-08-27T14:37:49+00:00"
|
||||
"time": "2025-06-18T08:56:18+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-file-iterator",
|
||||
@@ -10246,16 +10246,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "11.5.35",
|
||||
"version": "11.5.34",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||
"reference": "d341ee94ee5007b286fc7907b383aae6b5b3cc91"
|
||||
"reference": "3e4c6ef395f7cb61a6206c23e0e04b31724174f2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d341ee94ee5007b286fc7907b383aae6b5b3cc91",
|
||||
"reference": "d341ee94ee5007b286fc7907b383aae6b5b3cc91",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3e4c6ef395f7cb61a6206c23e0e04b31724174f2",
|
||||
"reference": "3e4c6ef395f7cb61a6206c23e0e04b31724174f2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -10269,7 +10269,7 @@
|
||||
"phar-io/manifest": "^2.0.4",
|
||||
"phar-io/version": "^3.2.1",
|
||||
"php": ">=8.2",
|
||||
"phpunit/php-code-coverage": "^11.0.11",
|
||||
"phpunit/php-code-coverage": "^11.0.10",
|
||||
"phpunit/php-file-iterator": "^5.1.0",
|
||||
"phpunit/php-invoker": "^5.0.1",
|
||||
"phpunit/php-text-template": "^4.0.1",
|
||||
@@ -10327,7 +10327,7 @@
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
|
||||
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.35"
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.34"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -10351,7 +10351,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-08-28T05:13:54+00:00"
|
||||
"time": "2025-08-20T14:41:45+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/cli-parser",
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ return [
|
||||
*/
|
||||
'components' => [
|
||||
'spotlight' => [
|
||||
'class' => 'App\Support\Spotlight',
|
||||
'class' => App\Support\Spotlight::class,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -122,21 +122,19 @@ class TransactionFactory extends Factory
|
||||
]);
|
||||
}
|
||||
|
||||
public function buy($quantity = 1): static
|
||||
public function buy(): 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($quantity = 1): static
|
||||
public function sell(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'transaction_type' => 'SELL',
|
||||
'quantity' => $quantity,
|
||||
'sale_price' => $this->faker->randomFloat(2, 10, 500),
|
||||
'cost_basis' => null,
|
||||
]);
|
||||
|
||||
@@ -52,10 +52,10 @@ new class extends Component
|
||||
{{ Number::currency($holding->dividends_earned ?? 0, $holding->market_data->currency) }}
|
||||
</p>
|
||||
|
||||
<p class="pt-2 text-sm" title="{{ \Carbon\Carbon::parse($holding->market_data->updated_at)->toIso8601String() }}">
|
||||
<p class="pt-2 text-sm" title="{{ \Illuminate\Support\Carbon::parse($holding->market_data->updated_at)->toIso8601String() }}">
|
||||
{{ __('Last Refreshed') }}:
|
||||
{{ !is_null($holding->market_data->updated_at)
|
||||
? \Carbon\Carbon::parse($holding->market_data->updated_at)->diffForHumans()
|
||||
? \Illuminate\Support\Carbon::parse($holding->market_data->updated_at)->diffForHumans()
|
||||
: '' }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -98,6 +98,6 @@ new class extends Component
|
||||
{{ Number::currency($row->dividends_earned ?? 0, $row->market_data->currency) }}
|
||||
@endscope
|
||||
@scope('cell_market_data_updated_at', $row)
|
||||
{{ \Carbon\Carbon::parse($row->market_data_updated_at)->diffForHumans() }}
|
||||
{{ \Illuminate\Support\Carbon::parse($row->market_data_updated_at)->diffForHumans() }}
|
||||
@endscope
|
||||
</x-table>
|
||||
|
||||
@@ -19,7 +19,7 @@ new class extends Component
|
||||
// props
|
||||
public ?Portfolio $portfolio;
|
||||
|
||||
public ?Transaction $transaction = null;
|
||||
public ?Transaction $transaction;
|
||||
|
||||
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, $this->transaction),
|
||||
new QuantityValidationRule($this->portfolio, $this->symbol, $this->transaction_type, $this->date),
|
||||
],
|
||||
'currency' => ['required', 'exists:currencies,currency'],
|
||||
'cost_basis' => 'exclude_if:transaction_type,SELL|min:0|numeric',
|
||||
|
||||
@@ -114,35 +114,6 @@ 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);
|
||||
|
||||
@@ -8,7 +8,6 @@ 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
|
||||
@@ -70,22 +69,4 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user