action = new CreateBucketAction; $this->scenario = Scenario::factory()->create(); } public function test_can_create_fixed_limit_bucket(): void { $bucket = $this->action->execute( $this->scenario, 'Test Bucket', BucketAllocationTypeEnum::FIXED_LIMIT, 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); $this->assertEquals(1, $bucket->sort_order); $this->assertEquals($this->scenario->id, $bucket->scenario_id); } public function test_can_create_percentage_bucket(): void { $bucket = $this->action->execute( $this->scenario, 'Percentage Bucket', BucketAllocationTypeEnum::PERCENTAGE, allocationValue: 25.5 ); $this->assertEquals(BucketAllocationTypeEnum::PERCENTAGE, $bucket->allocation_type); $this->assertEquals(25.5, $bucket->allocation_value); } public function test_can_create_unlimited_overflow_bucket(): void { $bucket = $this->action->execute( $this->scenario, '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_can_create_need_bucket(): void { $bucket = $this->action->execute( $this->scenario, 'Need Bucket', BucketAllocationTypeEnum::FIXED_LIMIT, BucketTypeEnum::NEED, 500 ); $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 { $bucket1 = $this->action->execute( $this->scenario, 'First Bucket', BucketAllocationTypeEnum::FIXED_LIMIT, allocationValue: 100 ); $bucket2 = $this->action->execute( $this->scenario, 'Second Bucket', BucketAllocationTypeEnum::FIXED_LIMIT, allocationValue: 200 ); $this->assertEquals(1, $bucket1->priority); $this->assertEquals(2, $bucket2->priority); } public function test_can_specify_custom_priority(): void { $bucket = $this->action->execute( $this->scenario, 'Priority Bucket', BucketAllocationTypeEnum::FIXED_LIMIT, allocationValue: 100, priority: 5 ); $this->assertEquals(5, $bucket->priority); } public function test_existing_priorities_are_shifted_when_inserting(): void { // Create initial buckets $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, allocationValue: 150, priority: 2); // Refresh models from database $bucket1->refresh(); $bucket2->refresh(); $bucket3->refresh(); // Check that priorities were shifted correctly $this->assertEquals(1, $bucket1->priority); $this->assertEquals(2, $newBucket->priority); $this->assertEquals(3, $bucket2->priority); // Shifted from 2 to 3 $this->assertEquals(4, $bucket3->priority); // Shifted from 3 to 4 } public function test_throws_exception_for_fixed_limit_without_allocation_value(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Fixed limit buckets require an allocation value'); $this->action->execute( $this->scenario, 'Test Bucket', BucketAllocationTypeEnum::FIXED_LIMIT ); } public function test_throws_exception_for_negative_fixed_limit_value(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Fixed limit allocation value must be non-negative'); $this->action->execute( $this->scenario, 'Test Bucket', BucketAllocationTypeEnum::FIXED_LIMIT, allocationValue: -100 ); } public function test_throws_exception_for_percentage_without_allocation_value(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Percentage buckets require an allocation value'); $this->action->execute( $this->scenario, 'Test Bucket', BucketAllocationTypeEnum::PERCENTAGE ); } public function test_throws_exception_for_percentage_below_minimum(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Percentage allocation value must be between 0.01 and 100'); $this->action->execute( $this->scenario, 'Test Bucket', BucketAllocationTypeEnum::PERCENTAGE, allocationValue: 0.005 ); } public function test_throws_exception_for_percentage_above_maximum(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Percentage allocation value must be between 0.01 and 100'); $this->action->execute( $this->scenario, 'Test Bucket', BucketAllocationTypeEnum::PERCENTAGE, allocationValue: 101 ); } public function test_throws_exception_for_negative_priority(): void { $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Priority must be at least 1'); $this->action->execute( $this->scenario, 'Test Bucket', BucketAllocationTypeEnum::FIXED_LIMIT, allocationValue: 100, priority: 0 ); } public function test_create_default_buckets(): void { $this->action->createDefaultBuckets($this->scenario); $buckets = $this->scenario->buckets()->orderBy('priority')->get(); $this->assertCount(3, $buckets); // 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 - 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); // 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); } 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, 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(); $this->assertCount(2, $buckets); $this->assertEquals('Bucket 2', $buckets[0]->name); // New bucket at priority 1 $this->assertEquals('Bucket 1', $buckets[1]->name); // Original bucket shifted to priority 2 } }