4 - Add bucket type parameter to CreateBucketAction and update CreateScenarioAction

This commit is contained in:
myrmidex 2026-03-19 21:22:09 +01:00
parent fe5355d182
commit da036ce97f
3 changed files with 148 additions and 44 deletions

View file

@ -3,6 +3,7 @@
namespace App\Actions; namespace App\Actions;
use App\Enums\BucketAllocationTypeEnum; use App\Enums\BucketAllocationTypeEnum;
use App\Enums\BucketTypeEnum;
use App\Models\Bucket; use App\Models\Bucket;
use App\Models\Scenario; use App\Models\Scenario;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@ -14,9 +15,13 @@ public function execute(
Scenario $scenario, Scenario $scenario,
string $name, string $name,
BucketAllocationTypeEnum $allocationType, BucketAllocationTypeEnum $allocationType,
BucketTypeEnum $type = BucketTypeEnum::NEED,
?float $allocationValue = null, ?float $allocationValue = null,
?int $priority = null ?int $priority = null,
): Bucket { ): Bucket {
// Validate type + allocation type constraints
$this->validateTypeConstraints($type, $allocationType);
// Validate allocation value based on type // Validate allocation value based on type
$this->validateAllocationValue($allocationType, $allocationValue); $this->validateAllocationValue($allocationType, $allocationValue);
@ -25,7 +30,7 @@ public function execute(
$allocationValue = null; $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) // Determine priority (append to end if not specified)
if ($priority === null) { if ($priority === null) {
$maxPriority = $scenario->buckets()->max('priority') ?? 0; $maxPriority = $scenario->buckets()->max('priority') ?? 0;
@ -53,6 +58,7 @@ public function execute(
// Create the bucket // Create the bucket
return $scenario->buckets()->create([ return $scenario->buckets()->create([
'name' => $name, 'name' => $name,
'type' => $type,
'priority' => $priority, 'priority' => $priority,
'sort_order' => $priority, // Start with sort_order matching priority 'sort_order' => $priority, // Start with sort_order matching priority
'allocation_type' => $allocationType, '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. * Validate allocation value based on allocation type.
*/ */
@ -99,29 +119,32 @@ public function createDefaultBuckets(Scenario $scenario): array
{ {
$buckets = []; $buckets = [];
// Monthly Expenses - Fixed limit, priority 1 // Monthly Expenses - Need, fixed limit, priority 1
$buckets[] = $this->execute( $buckets[] = $this->execute(
$scenario, $scenario,
'Monthly Expenses', 'Monthly Expenses',
BucketAllocationTypeEnum::FIXED_LIMIT, BucketAllocationTypeEnum::FIXED_LIMIT,
BucketTypeEnum::NEED,
0, 0,
1 1
); );
// Emergency Fund - Fixed limit, priority 2 // Emergency Fund - Need, fixed limit, priority 2
$buckets[] = $this->execute( $buckets[] = $this->execute(
$scenario, $scenario,
'Emergency Fund', 'Emergency Fund',
BucketAllocationTypeEnum::FIXED_LIMIT, BucketAllocationTypeEnum::FIXED_LIMIT,
BucketTypeEnum::NEED,
0, 0,
2 2
); );
// Investments - Unlimited, priority 3 // Overflow - Overflow, unlimited, priority 3
$buckets[] = $this->execute( $buckets[] = $this->execute(
$scenario, $scenario,
'Investments', 'Overflow',
BucketAllocationTypeEnum::UNLIMITED, BucketAllocationTypeEnum::UNLIMITED,
BucketTypeEnum::OVERFLOW,
null, null,
3 3
); );

View file

@ -2,7 +2,6 @@
namespace App\Actions; namespace App\Actions;
use App\Enums\BucketAllocationTypeEnum;
use App\Models\Scenario; use App\Models\Scenario;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@ -24,8 +23,6 @@ public function execute(array $data): Scenario
private function createDefaultBuckets(Scenario $scenario): void private function createDefaultBuckets(Scenario $scenario): void
{ {
$this->createBucketAction->execute($scenario, 'Monthly Expenses', BucketAllocationTypeEnum::FIXED_LIMIT, 0, 1); $this->createBucketAction->createDefaultBuckets($scenario);
$this->createBucketAction->execute($scenario, 'Emergency Fund', BucketAllocationTypeEnum::FIXED_LIMIT, 0, 2);
$this->createBucketAction->execute($scenario, 'Investments', BucketAllocationTypeEnum::UNLIMITED, null, 3);
} }
} }

View file

@ -4,6 +4,7 @@
use App\Actions\CreateBucketAction; use App\Actions\CreateBucketAction;
use App\Enums\BucketAllocationTypeEnum; use App\Enums\BucketAllocationTypeEnum;
use App\Enums\BucketTypeEnum;
use App\Models\Bucket; use App\Models\Bucket;
use App\Models\Scenario; use App\Models\Scenario;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
@ -31,11 +32,12 @@ public function test_can_create_fixed_limit_bucket(): void
$this->scenario, $this->scenario,
'Test Bucket', 'Test Bucket',
BucketAllocationTypeEnum::FIXED_LIMIT, BucketAllocationTypeEnum::FIXED_LIMIT,
1000.00 allocationValue: 1000.00
); );
$this->assertInstanceOf(Bucket::class, $bucket); $this->assertInstanceOf(Bucket::class, $bucket);
$this->assertEquals('Test Bucket', $bucket->name); $this->assertEquals('Test Bucket', $bucket->name);
$this->assertEquals(BucketTypeEnum::NEED, $bucket->type);
$this->assertEquals(BucketAllocationTypeEnum::FIXED_LIMIT, $bucket->allocation_type); $this->assertEquals(BucketAllocationTypeEnum::FIXED_LIMIT, $bucket->allocation_type);
$this->assertEquals(1000.00, $bucket->allocation_value); $this->assertEquals(1000.00, $bucket->allocation_value);
$this->assertEquals(1, $bucket->priority); $this->assertEquals(1, $bucket->priority);
@ -49,36 +51,117 @@ public function test_can_create_percentage_bucket(): void
$this->scenario, $this->scenario,
'Percentage Bucket', 'Percentage Bucket',
BucketAllocationTypeEnum::PERCENTAGE, BucketAllocationTypeEnum::PERCENTAGE,
25.5 allocationValue: 25.5
); );
$this->assertEquals(BucketAllocationTypeEnum::PERCENTAGE, $bucket->allocation_type); $this->assertEquals(BucketAllocationTypeEnum::PERCENTAGE, $bucket->allocation_type);
$this->assertEquals(25.5, $bucket->allocation_value); $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( $bucket = $this->action->execute(
$this->scenario, $this->scenario,
'Unlimited Bucket', 'Overflow Bucket',
BucketAllocationTypeEnum::UNLIMITED 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->assertEquals(BucketAllocationTypeEnum::UNLIMITED, $bucket->allocation_type);
$this->assertNull($bucket->allocation_value); $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( $bucket = $this->action->execute(
$this->scenario, $this->scenario,
'Unlimited Bucket', 'Need Bucket',
BucketAllocationTypeEnum::UNLIMITED, BucketAllocationTypeEnum::FIXED_LIMIT,
999.99 // This should be ignored and set to null BucketTypeEnum::NEED,
500
); );
$this->assertEquals(BucketAllocationTypeEnum::UNLIMITED, $bucket->allocation_type); $this->assertEquals(BucketTypeEnum::NEED, $bucket->type);
$this->assertNull($bucket->allocation_value); }
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 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, $this->scenario,
'First Bucket', 'First Bucket',
BucketAllocationTypeEnum::FIXED_LIMIT, BucketAllocationTypeEnum::FIXED_LIMIT,
100 allocationValue: 100
); );
$bucket2 = $this->action->execute( $bucket2 = $this->action->execute(
$this->scenario, $this->scenario,
'Second Bucket', 'Second Bucket',
BucketAllocationTypeEnum::FIXED_LIMIT, BucketAllocationTypeEnum::FIXED_LIMIT,
200 allocationValue: 200
); );
$this->assertEquals(1, $bucket1->priority); $this->assertEquals(1, $bucket1->priority);
@ -107,8 +190,8 @@ public function test_can_specify_custom_priority(): void
$this->scenario, $this->scenario,
'Priority Bucket', 'Priority Bucket',
BucketAllocationTypeEnum::FIXED_LIMIT, BucketAllocationTypeEnum::FIXED_LIMIT,
100, allocationValue: 100,
5 priority: 5
); );
$this->assertEquals(5, $bucket->priority); $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 public function test_existing_priorities_are_shifted_when_inserting(): void
{ {
// Create initial buckets // Create initial buckets
$bucket1 = $this->action->execute($this->scenario, 'Bucket 1', BucketAllocationTypeEnum::FIXED_LIMIT, 100, 1); $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, 200, 2); $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, 300, 3); $bucket3 = $this->action->execute($this->scenario, 'Bucket 3', BucketAllocationTypeEnum::FIXED_LIMIT, allocationValue: 300, priority: 3);
// Insert a bucket at priority 2 // 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 // Refresh models from database
$bucket1->refresh(); $bucket1->refresh();
@ -144,8 +227,7 @@ public function test_throws_exception_for_fixed_limit_without_allocation_value()
$this->action->execute( $this->action->execute(
$this->scenario, $this->scenario,
'Test Bucket', 'Test Bucket',
BucketAllocationTypeEnum::FIXED_LIMIT, BucketAllocationTypeEnum::FIXED_LIMIT
null
); );
} }
@ -158,7 +240,7 @@ public function test_throws_exception_for_negative_fixed_limit_value(): void
$this->scenario, $this->scenario,
'Test Bucket', 'Test Bucket',
BucketAllocationTypeEnum::FIXED_LIMIT, BucketAllocationTypeEnum::FIXED_LIMIT,
-100 allocationValue: -100
); );
} }
@ -170,8 +252,7 @@ public function test_throws_exception_for_percentage_without_allocation_value():
$this->action->execute( $this->action->execute(
$this->scenario, $this->scenario,
'Test Bucket', 'Test Bucket',
BucketAllocationTypeEnum::PERCENTAGE, BucketAllocationTypeEnum::PERCENTAGE
null
); );
} }
@ -184,7 +265,7 @@ public function test_throws_exception_for_percentage_below_minimum(): void
$this->scenario, $this->scenario,
'Test Bucket', 'Test Bucket',
BucketAllocationTypeEnum::PERCENTAGE, BucketAllocationTypeEnum::PERCENTAGE,
0.005 allocationValue: 0.005
); );
} }
@ -197,7 +278,7 @@ public function test_throws_exception_for_percentage_above_maximum(): void
$this->scenario, $this->scenario,
'Test Bucket', 'Test Bucket',
BucketAllocationTypeEnum::PERCENTAGE, BucketAllocationTypeEnum::PERCENTAGE,
101 allocationValue: 101
); );
} }
@ -210,8 +291,8 @@ public function test_throws_exception_for_negative_priority(): void
$this->scenario, $this->scenario,
'Test Bucket', 'Test Bucket',
BucketAllocationTypeEnum::FIXED_LIMIT, BucketAllocationTypeEnum::FIXED_LIMIT,
100, allocationValue: 100,
0 priority: 0
); );
} }
@ -223,20 +304,23 @@ public function test_create_default_buckets(): void
$this->assertCount(3, $buckets); $this->assertCount(3, $buckets);
// Monthly Expenses // Monthly Expenses - Need
$this->assertEquals('Monthly Expenses', $buckets[0]->name); $this->assertEquals('Monthly Expenses', $buckets[0]->name);
$this->assertEquals(BucketTypeEnum::NEED, $buckets[0]->type);
$this->assertEquals(1, $buckets[0]->priority); $this->assertEquals(1, $buckets[0]->priority);
$this->assertEquals(BucketAllocationTypeEnum::FIXED_LIMIT, $buckets[0]->allocation_type); $this->assertEquals(BucketAllocationTypeEnum::FIXED_LIMIT, $buckets[0]->allocation_type);
$this->assertEquals(0, $buckets[0]->allocation_value); $this->assertEquals(0, $buckets[0]->allocation_value);
// Emergency Fund // Emergency Fund - Need
$this->assertEquals('Emergency Fund', $buckets[1]->name); $this->assertEquals('Emergency Fund', $buckets[1]->name);
$this->assertEquals(BucketTypeEnum::NEED, $buckets[1]->type);
$this->assertEquals(2, $buckets[1]->priority); $this->assertEquals(2, $buckets[1]->priority);
$this->assertEquals(BucketAllocationTypeEnum::FIXED_LIMIT, $buckets[1]->allocation_type); $this->assertEquals(BucketAllocationTypeEnum::FIXED_LIMIT, $buckets[1]->allocation_type);
$this->assertEquals(0, $buckets[1]->allocation_value); $this->assertEquals(0, $buckets[1]->allocation_value);
// Investments // Overflow
$this->assertEquals('Investments', $buckets[2]->name); $this->assertEquals('Overflow', $buckets[2]->name);
$this->assertEquals(BucketTypeEnum::OVERFLOW, $buckets[2]->type);
$this->assertEquals(3, $buckets[2]->priority); $this->assertEquals(3, $buckets[2]->priority);
$this->assertEquals(BucketAllocationTypeEnum::UNLIMITED, $buckets[2]->allocation_type); $this->assertEquals(BucketAllocationTypeEnum::UNLIMITED, $buckets[2]->allocation_type);
$this->assertNull($buckets[2]->allocation_value); $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 // This test ensures database consistency by creating multiple buckets
// and verifying they all exist with correct priorities // 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 1', BucketAllocationTypeEnum::FIXED_LIMIT, allocationValue: 100, priority: 1);
$this->action->execute($this->scenario, 'Bucket 2', BucketAllocationTypeEnum::FIXED_LIMIT, 200, 1); // Insert at 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 // Both buckets should exist with correct priorities
$buckets = $this->scenario->buckets()->orderBy('priority')->get(); $buckets = $this->scenario->buckets()->orderBy('priority')->get();