diff --git a/database/factories/TransactionFactory.php b/database/factories/TransactionFactory.php index a0c3cdc..27c8b2f 100644 --- a/database/factories/TransactionFactory.php +++ b/database/factories/TransactionFactory.php @@ -19,14 +19,18 @@ class TransactionFactory extends Factory */ public function definition(): array { + $transaction_type = $this->faker->randomElement(['BUY', 'SELL']); + return [ - 'symbol' => $this->faker->randomElement(['AAPL', 'GOOGL', 'AMZN']), - 'transaction_type' => static::$transaction_type = $this->faker->randomElement(['BUY', 'SELL']), - 'portfolio_id' => Portfolio::factory()->create(), + 'symbol' => $this->faker->randomElement(['AAPL', 'GOOG', 'AMZN']), + 'transaction_type' => $transaction_type, + 'portfolio_id' => Portfolio::factory()->create()->id, 'date' => $this->faker->date('Y-m-d'), - 'quantity' => $this->faker->randomFloat(2, 1, 100), - 'cost_basis' => $this->faker->randomFloat(2, 10, 500), - 'sale_price' => static::$transaction_type == 'SELL' + 'quantity' => 1, + 'cost_basis' => $transaction_type == 'BUY' + ? $this->faker->randomFloat(2, 10, 500) + : null, + 'sale_price' => $transaction_type == 'SELL' ? $this->faker->randomFloat(2, 10, 500) : null, ]; @@ -39,7 +43,7 @@ class TransactionFactory extends Factory ]); } - public function portfolios($portfolio_id): static + public function portfolio($portfolio_id): static { return $this->state(fn (array $attributes) => [ 'portfolio_id' => $portfolio_id, @@ -50,6 +54,8 @@ class TransactionFactory extends Factory { return $this->state(fn (array $attributes) => [ 'transaction_type' => 'BUY', + 'cost_basis' => $this->faker->randomFloat(2, 10, 500), + 'sale_price' => null ]); } @@ -57,6 +63,8 @@ class TransactionFactory extends Factory { return $this->state(fn (array $attributes) => [ 'transaction_type' => 'SELL', + 'sale_price' => $this->faker->randomFloat(2, 10, 500), + 'cost_basis' => null, ]); } } diff --git a/phpunit.xml b/phpunit.xml index 6dd89fc..ea39f6e 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -26,5 +26,6 @@ + diff --git a/tests/CaptureDailyChangeTest.php b/tests/CaptureDailyChangeTest.php new file mode 100644 index 0000000..527bbbe --- /dev/null +++ b/tests/CaptureDailyChangeTest.php @@ -0,0 +1,56 @@ +actingAs($user = User::factory()->create()); + + $this->portfolio = Portfolio::factory()->create(); + Transaction::factory(5)->buy()->portfolio($this->portfolio->id)->symbol('AAPL')->create(); + $this->transaction = Transaction::factory()->sell()->portfolio($this->portfolio->id)->symbol('AAPL')->create(); + } + + /** + */ + public function test_daily_change_for_portfolios() + { + // Run the command + Artisan::call('daily-change:capture'); + + // Assert the daily change was captured for the portfolio + $this->assertDatabaseHas('daily_change', [ + 'portfolio_id' => $this->portfolio->id, + ]); + + $output = Artisan::output(); + $this->assertStringContainsString('Capturing daily change for', $output); + + $daily_change = DailyChange::where([ + 'portfolio_id' => $this->portfolio->id, + ])->get(); + + $this->assertCount(1, $daily_change); + + $this->assertEqualsWithDelta( + $this->transaction->sale_price - $this->transaction->cost_basis, + $daily_change->first()->realized_gains, + 0.01 + ); + + } +} diff --git a/tests/DashboardTest.php b/tests/DashboardTest.php new file mode 100644 index 0000000..e675b0a --- /dev/null +++ b/tests/DashboardTest.php @@ -0,0 +1,77 @@ +create(); + $this->actingAs($user); + + Portfolio::factory(5)->create(); + + $this->assertCount(5, $user->portfolios); + } + + /** + */ + public function test_user_has_transactions(): void + { + $user = User::factory()->create(); + $this->actingAs($user); + + Transaction::factory(10)->create(); + + $this->assertCount(10, $user->transactions); + } + + /** + */ + public function test_user_has_holdings(): void + { + $user = User::factory()->create(); + $this->actingAs($user); + + $portfolio = Portfolio::factory()->create(); + + Transaction::factory(5)->symbol('AAPL')->portfolio($portfolio->id)->create(); + + $this->assertCount(1, $user->holdings); + } + + /** + */ + public function test_user_has_dashboard_metrics(): void + { + $user = User::factory()->create(); + $this->actingAs($user); + + $portfolio = Portfolio::factory()->create(); + + Transaction::factory(5)->buy()->portfolio($portfolio->id)->symbol('AAPL')->create(); + $transaction = Transaction::factory()->sell()->portfolio($portfolio->id)->symbol('AAPL')->create(); + + $metrics = Holding::query() + ->myHoldings() + ->withPortfolioMetrics() + ->first(); + + $this->assertEqualsWithDelta( + $transaction->sale_price - $transaction->cost_basis, + $metrics->realized_gain_dollars, + 0.01 + ); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index a169a5a..02df1c3 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -11,7 +11,7 @@ abstract class TestCase extends BaseTestCase { parent::setUp(); - Artisan::call('migrate'); + // } protected function tearDown(): void diff --git a/tests/TransactionsTest.php b/tests/TransactionsTest.php index 5beb9a2..98fc924 100644 --- a/tests/TransactionsTest.php +++ b/tests/TransactionsTest.php @@ -2,72 +2,78 @@ namespace Tests; +use Tests\TestCase; use App\Models\User; +use App\Models\Holding; use App\Models\Portfolio; use App\Models\Transaction; -use Tests\TestCase; use Illuminate\Foundation\Testing\RefreshDatabase; class TransactionsTest extends TestCase { use RefreshDatabase; + /** - * A basic test example. */ public function test_can_create_a_transaction(): void { - $user = User::factory()->create(); - $this->actingAs($user); + $this->actingAs($user = User::factory()->create()); - $transactions = Transaction::factory()->create(); + $transaction = Transaction::factory()->create(); + + $this->assertNotNull($transaction); + } + + /** + */ + public function test_sales_calculate_cost_basis(): void + { + $this->actingAs($user = User::factory()->create()); + + Transaction::factory(5)->buy()->symbol('AAPL')->create(); + + $transaction = Transaction::factory()->sell()->symbol('AAPL')->create(); + + $this->assertNotNull($transaction->cost_basis); + } + + /** + */ + public function test_purchases_dont_have_sale_price(): void + { + $this->actingAs($user = User::factory()->create()); + + $transaction = Transaction::factory()->buy()->create(); + + $this->assertNull($transaction->sale_price); + } + + /** + */ + public function test_transaction_synced_to_holding(): void + { + $this->actingAs($user = User::factory()->create()); + + $portfolio = Portfolio::factory()->create(); + + Transaction::factory(5)->buy()->portfolio($portfolio->id)->symbol('AAPL')->create(); + $transaction = Transaction::factory()->sell()->portfolio($portfolio->id)->symbol('AAPL')->create(); + + $this->assertDatabaseHas('holdings', [ + 'portfolio_id' => $portfolio->id, + 'symbol' => 'AAPL', + 'quantity' => 4 + ]); + + $holding = Holding::where([ + 'portfolio_id' => $portfolio->id, + 'symbol' => 'AAPL' + ])->first(); + + $this->assertEqualsWithDelta( + $holding->realized_gain_dollars, + $transaction->sale_price - $transaction->cost_basis, + 0.01 + ); } } - - - - - // static::saving(function ($transaction) { - - // if ($transaction->transaction_type == 'SELL') { - - // $transaction->ensureCostBasisIsAddedToSale(); - // } - // }); - - // static::saved(function ($transaction) { - - // $transaction->syncToHolding(); - - // $transaction->refreshMarketData(); - - // cache()->tags(['metrics', $transaction->portfolio_id])->flush(); - // }); - - // public function update() - // { - - // $this->transaction->update($this->validate()); - // // $this->transaction->owner_id = auth()->user()->id; - // $this->transaction->save(); - - // $this->success(__('Transaction updated')); - - // $this->dispatch('toggle-manage-transaction'); - // $this->dispatch('transaction-updated'); - // } - - // public function save() - // { - // $validated = $this->validate(); - - // if (!isset($this->portfolio)) { - // $this->portfolio = Portfolio::find($this->portfolio_id); - // } - - // $transaction = $this->portfolio->transactions()->create($validated); - // $transaction->save(); - - // $this->dispatch('transaction-saved'); - - // $this->success(__('Transaction created'), redirectTo: route('holding.show', ['portfolio' => $this->portfolio->id, 'symbol' => $this->symbol])); - // }