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;
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
);

View file

@ -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);
}
}

View file

@ -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();