Compare commits

...

6 Commits

Author SHA1 Message Date
hackerESQ a0bd776abb fix: quantity validation should not count current transaction 2025-08-29 15:47:38 -05:00
hackerESQ afcafa6031 chore: upgrade deps 2025-08-28 21:56:28 -05:00
hackerESQ 07c85697f3 chore: upgrade deps 2025-08-28 21:56:05 -05:00
hackerESQ a882b5aadb chore: clean up 2025-08-28 21:26:11 -05:00
hackerESQ bad82fb41b chore: cleanup old files 2025-08-28 21:26:11 -05:00
Shift 5aca9008cb Add .shift to open Pull Request 2025-08-28 21:26:11 -05:00
11 changed files with 98 additions and 60 deletions
+4
View File
@@ -0,0 +1,4 @@
This file was added by Shift #157267 in order to open a
Pull Request since no other commits were made.
You should remove this file.
+2 -1
View File
@@ -39,7 +39,8 @@ class TransactionRequest extends FormRequest
$this->input('portfolio'), $this->input('portfolio'),
$this->requestOrModelValue('symbol', 'transaction'), $this->requestOrModelValue('symbol', 'transaction'),
$this->requestOrModelValue('transaction_type', 'transaction'), $this->requestOrModelValue('transaction_type', 'transaction'),
$this->requestOrModelValue('date', 'transaction') $this->requestOrModelValue('date', 'transaction'),
$this->transaction
), ),
], ],
'currency' => ['required', 'exists:currencies,currency'], 'currency' => ['required', 'exists:currencies,currency'],
+5 -3
View File
@@ -6,8 +6,8 @@ namespace App\Rules;
use App\Models\Portfolio; use App\Models\Portfolio;
use App\Models\Transaction; use App\Models\Transaction;
use Illuminate\Support\Carbon;
use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Carbon;
class QuantityValidationRule implements ValidationRule class QuantityValidationRule implements ValidationRule
{ {
@@ -20,8 +20,9 @@ class QuantityValidationRule implements ValidationRule
protected ?Portfolio $portfolio, protected ?Portfolio $portfolio,
protected ?string $symbol, protected ?string $symbol,
protected ?string $transactionType, protected ?string $transactionType,
protected string|Carbon|null $date protected string|Carbon|null $date,
) { } protected ?Transaction $transaction
) {}
/** /**
* Validate the attribute. * Validate the attribute.
@@ -42,6 +43,7 @@ class QuantityValidationRule implements ValidationRule
->sum('quantity'); ->sum('quantity');
$sales_qty = (float) $this->portfolio->transactions() $sales_qty = (float) $this->portfolio->transactions()
->where('id', '!=', $this->transaction?->id)
->symbol($this->symbol) ->symbol($this->symbol)
->sell() ->sell()
->whereDate('date', '<', $this->date) ->whereDate('date', '<', $this->date)
-16
View File
@@ -1,16 +0,0 @@
<?php
declare(strict_types=1);
use App\Models\Currency;
if (! function_exists('currency')) {
// /**
// * Returns an instance of the currency model
// * */
// function currency(): Currency
// {
// return new Currency;
// }
}
+1 -4
View File
@@ -35,7 +35,7 @@
"laravel/sail": "^1.26", "laravel/sail": "^1.26",
"mockery/mockery": "^1.6", "mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.0", "nunomaduro/collision": "^8.0",
"phpunit/phpunit": "^11.0.1" "phpunit/phpunit": "^11.0"
}, },
"repositories": [ "repositories": [
{ {
@@ -55,9 +55,6 @@
} }
], ],
"autoload": { "autoload": {
"files": [
"app/Support/Helpers.php"
],
"psr-4": { "psr-4": {
"App\\": "app/", "App\\": "app/",
"Database\\Factories\\": "database/factories/", "Database\\Factories\\": "database/factories/",
Generated
+31 -31
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "d5d786656eb888c2966c648ddf946280", "content-hash": "09faf36704392ba5099e39fe62908057",
"packages": [ "packages": [
{ {
"name": "aws/aws-crt-php", "name": "aws/aws-crt-php",
@@ -62,16 +62,16 @@
}, },
{ {
"name": "aws/aws-sdk-php", "name": "aws/aws-sdk-php",
"version": "3.356.5", "version": "3.356.7",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/aws/aws-sdk-php.git", "url": "https://github.com/aws/aws-sdk-php.git",
"reference": "5872ccb5100c4afb0dae3db0bd46636f63ae8147" "reference": "6b44237a218485bf43a0015600aebf43cb726d4e"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/5872ccb5100c4afb0dae3db0bd46636f63ae8147", "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/6b44237a218485bf43a0015600aebf43cb726d4e",
"reference": "5872ccb5100c4afb0dae3db0bd46636f63ae8147", "reference": "6b44237a218485bf43a0015600aebf43cb726d4e",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -153,9 +153,9 @@
"support": { "support": {
"forum": "https://github.com/aws/aws-sdk-php/discussions", "forum": "https://github.com/aws/aws-sdk-php/discussions",
"issues": "https://github.com/aws/aws-sdk-php/issues", "issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.356.5" "source": "https://github.com/aws/aws-sdk-php/tree/3.356.7"
}, },
"time": "2025-08-26T18:05:04+00:00" "time": "2025-08-28T18:14:39+00:00"
}, },
{ {
"name": "bacon/bacon-qr-code", "name": "bacon/bacon-qr-code",
@@ -1803,21 +1803,21 @@
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/hackeresq/filter-models", "url": "https://github.com/hackeresq/filter-models",
"reference": "847950d3277fe7df3a2dcdcdd3ba37d3b07ee667" "reference": "e92c1e1e8af299cb2c3c3e6d7768f2fb2dcb9146"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/hackeresq/filter-models/zipball/847950d3277fe7df3a2dcdcdd3ba37d3b07ee667", "url": "https://api.github.com/repos/hackeresq/filter-models/zipball/e92c1e1e8af299cb2c3c3e6d7768f2fb2dcb9146",
"reference": "847950d3277fe7df3a2dcdcdd3ba37d3b07ee667", "reference": "e92c1e1e8af299cb2c3c3e6d7768f2fb2dcb9146",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"laravel/framework": "^11.9", "laravel/framework": "^11.0||^12.0",
"php": "^8.2" "php": "^8.2"
}, },
"require-dev": { "require-dev": {
"orchestra/testbench": "^9.9", "orchestra/testbench": "^9.9",
"phpunit/phpunit": "^10.0|^11.0" "phpunit/phpunit": "^10.0||^11.0"
}, },
"default-branch": true, "default-branch": true,
"type": "library", "type": "library",
@@ -1839,7 +1839,7 @@
} }
}, },
"description": "Simple package to filter your Laravel models with query parameters", "description": "Simple package to filter your Laravel models with query parameters",
"time": "2025-01-27T23:18:08+00:00" "time": "2025-08-29T02:40:27+00:00"
}, },
{ {
"name": "investbrainapp/frankfurter-client", "name": "investbrainapp/frankfurter-client",
@@ -1847,16 +1847,16 @@
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/investbrainapp/frankfurter-client", "url": "https://github.com/investbrainapp/frankfurter-client",
"reference": "738b2b53f48b7cdf4d66c44a592430dea4de9fd0" "reference": "d2a96d7db2d17e91245b0cf2146e2b8a295b8d4b"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/investbrainapp/frankfurter-client/zipball/738b2b53f48b7cdf4d66c44a592430dea4de9fd0", "url": "https://api.github.com/repos/investbrainapp/frankfurter-client/zipball/d2a96d7db2d17e91245b0cf2146e2b8a295b8d4b",
"reference": "738b2b53f48b7cdf4d66c44a592430dea4de9fd0", "reference": "d2a96d7db2d17e91245b0cf2146e2b8a295b8d4b",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"laravel/framework": "^11.9", "laravel/framework": "^11.0||^12.0",
"php": "^8.2" "php": "^8.2"
}, },
"default-branch": true, "default-branch": true,
@@ -1874,7 +1874,7 @@
} }
}, },
"description": "Laravel SDK for interacting with the Frankfurter currency exchange API", "description": "Laravel SDK for interacting with the Frankfurter currency exchange API",
"time": "2025-04-11T02:35:18+00:00" "time": "2025-08-29T02:39:41+00:00"
}, },
{ {
"name": "jfcherng/php-color-output", "name": "jfcherng/php-color-output",
@@ -9911,16 +9911,16 @@
}, },
{ {
"name": "phpunit/php-code-coverage", "name": "phpunit/php-code-coverage",
"version": "11.0.10", "version": "11.0.11",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "1a800a7446add2d79cc6b3c01c45381810367d76" "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/1a800a7446add2d79cc6b3c01c45381810367d76", "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4",
"reference": "1a800a7446add2d79cc6b3c01c45381810367d76", "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -9977,7 +9977,7 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/show" "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.11"
}, },
"funding": [ "funding": [
{ {
@@ -9997,7 +9997,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-06-18T08:56:18+00:00" "time": "2025-08-27T14:37:49+00:00"
}, },
{ {
"name": "phpunit/php-file-iterator", "name": "phpunit/php-file-iterator",
@@ -10246,16 +10246,16 @@
}, },
{ {
"name": "phpunit/phpunit", "name": "phpunit/phpunit",
"version": "11.5.34", "version": "11.5.35",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git", "url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "3e4c6ef395f7cb61a6206c23e0e04b31724174f2" "reference": "d341ee94ee5007b286fc7907b383aae6b5b3cc91"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3e4c6ef395f7cb61a6206c23e0e04b31724174f2", "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d341ee94ee5007b286fc7907b383aae6b5b3cc91",
"reference": "3e4c6ef395f7cb61a6206c23e0e04b31724174f2", "reference": "d341ee94ee5007b286fc7907b383aae6b5b3cc91",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -10269,7 +10269,7 @@
"phar-io/manifest": "^2.0.4", "phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1", "phar-io/version": "^3.2.1",
"php": ">=8.2", "php": ">=8.2",
"phpunit/php-code-coverage": "^11.0.10", "phpunit/php-code-coverage": "^11.0.11",
"phpunit/php-file-iterator": "^5.1.0", "phpunit/php-file-iterator": "^5.1.0",
"phpunit/php-invoker": "^5.0.1", "phpunit/php-invoker": "^5.0.1",
"phpunit/php-text-template": "^4.0.1", "phpunit/php-text-template": "^4.0.1",
@@ -10327,7 +10327,7 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues", "issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy", "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.34" "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.35"
}, },
"funding": [ "funding": [
{ {
@@ -10351,7 +10351,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-08-20T14:41:45+00:00" "time": "2025-08-28T05:13:54+00:00"
}, },
{ {
"name": "sebastian/cli-parser", "name": "sebastian/cli-parser",
+4 -2
View File
@@ -122,19 +122,21 @@ class TransactionFactory extends Factory
]); ]);
} }
public function buy(): static public function buy($quantity = 1): static
{ {
return $this->state(fn (array $attributes) => [ return $this->state(fn (array $attributes) => [
'transaction_type' => 'BUY', 'transaction_type' => 'BUY',
'quantity' => $quantity,
'cost_basis' => $this->faker->randomFloat(2, 10, 500), 'cost_basis' => $this->faker->randomFloat(2, 10, 500),
'sale_price' => null, 'sale_price' => null,
]); ]);
} }
public function sell(): static public function sell($quantity = 1): static
{ {
return $this->state(fn (array $attributes) => [ return $this->state(fn (array $attributes) => [
'transaction_type' => 'SELL', 'transaction_type' => 'SELL',
'quantity' => $quantity,
'sale_price' => $this->faker->randomFloat(2, 10, 500), 'sale_price' => $this->faker->randomFloat(2, 10, 500),
'cost_basis' => null, 'cost_basis' => null,
]); ]);
@@ -83,7 +83,7 @@ new class extends Component
<x-menu-item title="{{ __('Log Out') }}" icon="o-power" onclick="event.preventDefault(); document.getElementById('logout').submit();" /> <x-menu-item title="{{ __('Log Out') }}" icon="o-power" onclick="event.preventDefault(); document.getElementById('logout').submit();" />
<form id="logout" action="{{ route('logout') }}" method="POST" style="display: none;"> <form id="logout" action="{{ route('logout') }}" method="POST" style="display: none;">
{{ csrf_field() }} @csrf
</form> </form>
</x-dropdown> </x-dropdown>
@@ -19,7 +19,7 @@ new class extends Component
// props // props
public ?Portfolio $portfolio; public ?Portfolio $portfolio;
public ?Transaction $transaction; public ?Transaction $transaction = null;
public ?string $portfolio_id; public ?string $portfolio_id;
@@ -53,7 +53,7 @@ new class extends Component
'required', 'required',
'numeric', 'numeric',
'gt:0', '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'], 'currency' => ['required', 'exists:currencies,currency'],
'cost_basis' => 'exclude_if:transaction_type,SELL|min:0|numeric', 'cost_basis' => 'exclude_if:transaction_type,SELL|min:0|numeric',
+29
View File
@@ -114,6 +114,35 @@ class TransactionsTest extends TestCase
->assertJsonValidationErrors(['symbol']); ->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() public function test_can_show_a_transaction()
{ {
$this->actingAs($this->user); $this->actingAs($this->user);
+19
View File
@@ -8,6 +8,7 @@ use App\Models\Holding;
use App\Models\Portfolio; use App\Models\Portfolio;
use App\Models\Transaction; use App\Models\Transaction;
use App\Models\User; use App\Models\User;
use App\Rules\QuantityValidationRule;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
class TransactionsTest extends TestCase class TransactionsTest extends TestCase
@@ -69,4 +70,22 @@ class TransactionsTest extends TestCase
0.01 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);
}
} }