From da036ce97f579e99376ed257d711b2d10fa50a98 Mon Sep 17 00:00:00 2001 From: myrmidex Date: Thu, 19 Mar 2026 21:22:09 +0100 Subject: [PATCH] 4 - Add bucket type parameter to CreateBucketAction and update CreateScenarioAction --- app/Actions/CreateBucketAction.php | 35 +++- app/Actions/CreateScenarioAction.php | 5 +- tests/Unit/Actions/CreateBucketActionTest.php | 152 ++++++++++++++---- 3 files changed, 148 insertions(+), 44 deletions(-) diff --git a/app/Actions/CreateBucketAction.php b/app/Actions/CreateBucketAction.php index 18ff266..b3bf2b9 100644 --- a/app/Actions/CreateBucketAction.php +++ b/app/Actions/CreateBucketAction.php @@ -3,6 +3,7 @@ namespace App\Actions; use App\Enums\BucketAllocationTypeEnum; +use App\Enums\BucketTypeEnum; use App\Models\Bucket; use App\Models\Scenario; use Illuminate\Support\Facades\DB; @@ -14,9 +15,13 @@ public function execute( Scenario $scenario, string $name, BucketAllocationTypeEnum $allocationType, + BucketTypeEnum $type = BucketTypeEnum::NEED, ?float $allocationValue = null, - ?int $priority = null + ?int $priority = null, ): Bucket { + // Validate type + allocation type constraints + $this->validateTypeConstraints($type, $allocationType); + // Validate allocation value based on type $this->validateAllocationValue($allocationType, $allocationValue); @@ -25,7 +30,7 @@ public function execute( $allocationValue = null; } - return DB::transaction(function () use ($scenario, $name, $allocationType, $allocationValue, $priority) { + return DB::transaction(function () use ($scenario, $name, $allocationType, $allocationValue, $priority, $type) { // Determine priority (append to end if not specified) if ($priority === null) { $maxPriority = $scenario->buckets()->max('priority') ?? 0; @@ -53,6 +58,7 @@ public function execute( // Create the bucket return $scenario->buckets()->create([ 'name' => $name, + 'type' => $type, 'priority' => $priority, 'sort_order' => $priority, // Start with sort_order matching priority 'allocation_type' => $allocationType, @@ -61,6 +67,20 @@ public function execute( }); } + /** + * Validate that bucket type and allocation type are compatible. + */ + private function validateTypeConstraints(BucketTypeEnum $type, BucketAllocationTypeEnum $allocationType): void + { + if ($type === BucketTypeEnum::OVERFLOW && $allocationType !== BucketAllocationTypeEnum::UNLIMITED) { + throw new InvalidArgumentException('Overflow buckets must use unlimited allocation type'); + } + + if ($type !== BucketTypeEnum::OVERFLOW && $allocationType === BucketAllocationTypeEnum::UNLIMITED) { + throw new InvalidArgumentException('Only overflow buckets can use unlimited allocation type'); + } + } + /** * Validate allocation value based on allocation type. */ @@ -99,29 +119,32 @@ public function createDefaultBuckets(Scenario $scenario): array { $buckets = []; - // Monthly Expenses - Fixed limit, priority 1 + // Monthly Expenses - Need, fixed limit, priority 1 $buckets[] = $this->execute( $scenario, 'Monthly Expenses', BucketAllocationTypeEnum::FIXED_LIMIT, + BucketTypeEnum::NEED, 0, 1 ); - // Emergency Fund - Fixed limit, priority 2 + // Emergency Fund - Need, fixed limit, priority 2 $buckets[] = $this->execute( $scenario, 'Emergency Fund', BucketAllocationTypeEnum::FIXED_LIMIT, + BucketTypeEnum::NEED, 0, 2 ); - // Investments - Unlimited, priority 3 + // Overflow - Overflow, unlimited, priority 3 $buckets[] = $this->execute( $scenario, - 'Investments', + 'Overflow', BucketAllocationTypeEnum::UNLIMITED, + BucketTypeEnum::OVERFLOW, null, 3 ); diff --git a/app/Actions/CreateScenarioAction.php b/app/Actions/CreateScenarioAction.php index b6c0085..7c27b6f 100644 --- a/app/Actions/CreateScenarioAction.php +++ b/app/Actions/CreateScenarioAction.php @@ -2,7 +2,6 @@ namespace App\Actions; -use App\Enums\BucketAllocationTypeEnum; use App\Models\Scenario; use Illuminate\Support\Facades\DB; @@ -24,8 +23,6 @@ public function execute(array $data): Scenario private function createDefaultBuckets(Scenario $scenario): void { - $this->createBucketAction->execute($scenario, 'Monthly Expenses', BucketAllocationTypeEnum::FIXED_LIMIT, 0, 1); - $this->createBucketAction->execute($scenario, 'Emergency Fund', BucketAllocationTypeEnum::FIXED_LIMIT, 0, 2); - $this->createBucketAction->execute($scenario, 'Investments', BucketAllocationTypeEnum::UNLIMITED, null, 3); + $this->createBucketAction->createDefaultBuckets($scenario); } } diff --git a/tests/Unit/Actions/CreateBucketActionTest.php b/tests/Unit/Actions/CreateBucketActionTest.php index fdf5769..b1567b1 100644 --- a/tests/Unit/Actions/CreateBucketActionTest.php +++ b/tests/Unit/Actions/CreateBucketActionTest.php @@ -4,6 +4,7 @@ use App\Actions\CreateBucketAction; use App\Enums\BucketAllocationTypeEnum; +use App\Enums\BucketTypeEnum; use App\Models\Bucket; use App\Models\Scenario; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -31,11 +32,12 @@ public function test_can_create_fixed_limit_bucket(): void $this->scenario, 'Test Bucket', BucketAllocationTypeEnum::FIXED_LIMIT, - 1000.00 + allocationValue: 1000.00 ); $this->assertInstanceOf(Bucket::class, $bucket); $this->assertEquals('Test Bucket', $bucket->name); + $this->assertEquals(BucketTypeEnum::NEED, $bucket->type); $this->assertEquals(BucketAllocationTypeEnum::FIXED_LIMIT, $bucket->allocation_type); $this->assertEquals(1000.00, $bucket->allocation_value); $this->assertEquals(1, $bucket->priority); @@ -49,36 +51,117 @@ public function test_can_create_percentage_bucket(): void $this->scenario, 'Percentage Bucket', BucketAllocationTypeEnum::PERCENTAGE, - 25.5 + allocationValue: 25.5 ); $this->assertEquals(BucketAllocationTypeEnum::PERCENTAGE, $bucket->allocation_type); $this->assertEquals(25.5, $bucket->allocation_value); } - public function test_can_create_unlimited_bucket(): void + public function test_can_create_unlimited_overflow_bucket(): void { $bucket = $this->action->execute( $this->scenario, - 'Unlimited Bucket', - BucketAllocationTypeEnum::UNLIMITED + 'Overflow Bucket', + BucketAllocationTypeEnum::UNLIMITED, + BucketTypeEnum::OVERFLOW + ); + + $this->assertEquals(BucketTypeEnum::OVERFLOW, $bucket->type); + $this->assertEquals(BucketAllocationTypeEnum::UNLIMITED, $bucket->allocation_type); + $this->assertNull($bucket->allocation_value); + } + + public function test_unlimited_overflow_bucket_ignores_allocation_value(): void + { + $bucket = $this->action->execute( + $this->scenario, + 'Overflow Bucket', + BucketAllocationTypeEnum::UNLIMITED, + BucketTypeEnum::OVERFLOW, + 999.99 ); $this->assertEquals(BucketAllocationTypeEnum::UNLIMITED, $bucket->allocation_type); $this->assertNull($bucket->allocation_value); } - public function test_unlimited_bucket_ignores_allocation_value(): void + public function test_can_create_need_bucket(): void { $bucket = $this->action->execute( $this->scenario, - 'Unlimited Bucket', - BucketAllocationTypeEnum::UNLIMITED, - 999.99 // This should be ignored and set to null + 'Need Bucket', + BucketAllocationTypeEnum::FIXED_LIMIT, + BucketTypeEnum::NEED, + 500 ); - $this->assertEquals(BucketAllocationTypeEnum::UNLIMITED, $bucket->allocation_type); - $this->assertNull($bucket->allocation_value); + $this->assertEquals(BucketTypeEnum::NEED, $bucket->type); + } + + public function test_can_create_want_bucket(): void + { + $bucket = $this->action->execute( + $this->scenario, + 'Want Bucket', + BucketAllocationTypeEnum::FIXED_LIMIT, + BucketTypeEnum::WANT, + 500 + ); + + $this->assertEquals(BucketTypeEnum::WANT, $bucket->type); + } + + public function test_default_type_is_need(): void + { + $bucket = $this->action->execute( + $this->scenario, + 'Default Type Bucket', + BucketAllocationTypeEnum::FIXED_LIMIT, + allocationValue: 100 + ); + + $this->assertEquals(BucketTypeEnum::NEED, $bucket->type); + } + + public function test_overflow_bucket_must_use_unlimited_allocation(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Overflow buckets must use unlimited allocation type'); + + $this->action->execute( + $this->scenario, + 'Bad Overflow', + BucketAllocationTypeEnum::FIXED_LIMIT, + BucketTypeEnum::OVERFLOW, + 100 + ); + } + + public function test_non_overflow_bucket_cannot_use_unlimited_allocation(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Only overflow buckets can use unlimited allocation type'); + + $this->action->execute( + $this->scenario, + 'Bad Need Bucket', + BucketAllocationTypeEnum::UNLIMITED, + BucketTypeEnum::NEED + ); + } + + public function test_want_bucket_cannot_use_unlimited_allocation(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Only overflow buckets can use unlimited allocation type'); + + $this->action->execute( + $this->scenario, + 'Bad Want Bucket', + BucketAllocationTypeEnum::UNLIMITED, + BucketTypeEnum::WANT + ); } public function test_priority_auto_increments_when_not_specified(): void @@ -87,14 +170,14 @@ public function test_priority_auto_increments_when_not_specified(): void $this->scenario, 'First Bucket', BucketAllocationTypeEnum::FIXED_LIMIT, - 100 + allocationValue: 100 ); $bucket2 = $this->action->execute( $this->scenario, 'Second Bucket', BucketAllocationTypeEnum::FIXED_LIMIT, - 200 + allocationValue: 200 ); $this->assertEquals(1, $bucket1->priority); @@ -107,8 +190,8 @@ public function test_can_specify_custom_priority(): void $this->scenario, 'Priority Bucket', BucketAllocationTypeEnum::FIXED_LIMIT, - 100, - 5 + allocationValue: 100, + priority: 5 ); $this->assertEquals(5, $bucket->priority); @@ -117,12 +200,12 @@ public function test_can_specify_custom_priority(): void public function test_existing_priorities_are_shifted_when_inserting(): void { // Create initial buckets - $bucket1 = $this->action->execute($this->scenario, 'Bucket 1', BucketAllocationTypeEnum::FIXED_LIMIT, 100, 1); - $bucket2 = $this->action->execute($this->scenario, 'Bucket 2', BucketAllocationTypeEnum::FIXED_LIMIT, 200, 2); - $bucket3 = $this->action->execute($this->scenario, 'Bucket 3', BucketAllocationTypeEnum::FIXED_LIMIT, 300, 3); + $bucket1 = $this->action->execute($this->scenario, 'Bucket 1', BucketAllocationTypeEnum::FIXED_LIMIT, allocationValue: 100, priority: 1); + $bucket2 = $this->action->execute($this->scenario, 'Bucket 2', BucketAllocationTypeEnum::FIXED_LIMIT, allocationValue: 200, priority: 2); + $bucket3 = $this->action->execute($this->scenario, 'Bucket 3', BucketAllocationTypeEnum::FIXED_LIMIT, allocationValue: 300, priority: 3); // Insert a bucket at priority 2 - $newBucket = $this->action->execute($this->scenario, 'New Bucket', BucketAllocationTypeEnum::FIXED_LIMIT, 150, 2); + $newBucket = $this->action->execute($this->scenario, 'New Bucket', BucketAllocationTypeEnum::FIXED_LIMIT, allocationValue: 150, priority: 2); // Refresh models from database $bucket1->refresh(); @@ -144,8 +227,7 @@ public function test_throws_exception_for_fixed_limit_without_allocation_value() $this->action->execute( $this->scenario, 'Test Bucket', - BucketAllocationTypeEnum::FIXED_LIMIT, - null + BucketAllocationTypeEnum::FIXED_LIMIT ); } @@ -158,7 +240,7 @@ public function test_throws_exception_for_negative_fixed_limit_value(): void $this->scenario, 'Test Bucket', BucketAllocationTypeEnum::FIXED_LIMIT, - -100 + allocationValue: -100 ); } @@ -170,8 +252,7 @@ public function test_throws_exception_for_percentage_without_allocation_value(): $this->action->execute( $this->scenario, 'Test Bucket', - BucketAllocationTypeEnum::PERCENTAGE, - null + BucketAllocationTypeEnum::PERCENTAGE ); } @@ -184,7 +265,7 @@ public function test_throws_exception_for_percentage_below_minimum(): void $this->scenario, 'Test Bucket', BucketAllocationTypeEnum::PERCENTAGE, - 0.005 + allocationValue: 0.005 ); } @@ -197,7 +278,7 @@ public function test_throws_exception_for_percentage_above_maximum(): void $this->scenario, 'Test Bucket', BucketAllocationTypeEnum::PERCENTAGE, - 101 + allocationValue: 101 ); } @@ -210,8 +291,8 @@ public function test_throws_exception_for_negative_priority(): void $this->scenario, 'Test Bucket', BucketAllocationTypeEnum::FIXED_LIMIT, - 100, - 0 + allocationValue: 100, + priority: 0 ); } @@ -223,20 +304,23 @@ public function test_create_default_buckets(): void $this->assertCount(3, $buckets); - // Monthly Expenses + // Monthly Expenses - Need $this->assertEquals('Monthly Expenses', $buckets[0]->name); + $this->assertEquals(BucketTypeEnum::NEED, $buckets[0]->type); $this->assertEquals(1, $buckets[0]->priority); $this->assertEquals(BucketAllocationTypeEnum::FIXED_LIMIT, $buckets[0]->allocation_type); $this->assertEquals(0, $buckets[0]->allocation_value); - // Emergency Fund + // Emergency Fund - Need $this->assertEquals('Emergency Fund', $buckets[1]->name); + $this->assertEquals(BucketTypeEnum::NEED, $buckets[1]->type); $this->assertEquals(2, $buckets[1]->priority); $this->assertEquals(BucketAllocationTypeEnum::FIXED_LIMIT, $buckets[1]->allocation_type); $this->assertEquals(0, $buckets[1]->allocation_value); - // Investments - $this->assertEquals('Investments', $buckets[2]->name); + // Overflow + $this->assertEquals('Overflow', $buckets[2]->name); + $this->assertEquals(BucketTypeEnum::OVERFLOW, $buckets[2]->type); $this->assertEquals(3, $buckets[2]->priority); $this->assertEquals(BucketAllocationTypeEnum::UNLIMITED, $buckets[2]->allocation_type); $this->assertNull($buckets[2]->allocation_value); @@ -246,8 +330,8 @@ public function test_creates_buckets_in_database_transaction(): void { // This test ensures database consistency by creating multiple buckets // and verifying they all exist with correct priorities - $this->action->execute($this->scenario, 'Bucket 1', BucketAllocationTypeEnum::FIXED_LIMIT, 100, 1); - $this->action->execute($this->scenario, 'Bucket 2', BucketAllocationTypeEnum::FIXED_LIMIT, 200, 1); // Insert at priority 1 + $this->action->execute($this->scenario, 'Bucket 1', BucketAllocationTypeEnum::FIXED_LIMIT, allocationValue: 100, priority: 1); + $this->action->execute($this->scenario, 'Bucket 2', BucketAllocationTypeEnum::FIXED_LIMIT, allocationValue: 200, priority: 1); // Insert at priority 1 // Both buckets should exist with correct priorities $buckets = $this->scenario->buckets()->orderBy('priority')->get();