buckets/app/Actions/CreateBucketAction.php

171 lines
6.2 KiB
PHP
Raw Normal View History

2025-12-29 23:32:05 +01:00
<?php
namespace App\Actions;
2025-12-31 02:34:30 +01:00
use App\Enums\BucketAllocationTypeEnum;
use App\Enums\BucketTypeEnum;
2025-12-29 23:32:05 +01:00
use App\Models\Bucket;
use App\Models\Scenario;
use Illuminate\Support\Facades\DB;
use InvalidArgumentException;
class CreateBucketAction
{
public function execute(
Scenario $scenario,
string $name,
2025-12-31 02:34:30 +01:00
BucketAllocationTypeEnum $allocationType,
BucketTypeEnum $type = BucketTypeEnum::NEED,
2025-12-29 23:32:05 +01:00
?float $allocationValue = null,
?int $priority = null,
?float $bufferMultiplier = null,
2025-12-29 23:32:05 +01:00
): Bucket {
// Validate type + allocation type constraints
$this->validateTypeConstraints($type, $allocationType);
// Enforce one overflow bucket per scenario
if ($type === BucketTypeEnum::OVERFLOW && $scenario->buckets()->where('type', BucketTypeEnum::OVERFLOW->value)->exists()) {
throw new InvalidArgumentException('A scenario can only have one overflow bucket');
}
2025-12-29 23:32:05 +01:00
// Validate allocation value based on type
$this->validateAllocationValue($allocationType, $allocationValue);
// Validate and normalize buffer multiplier
$bufferMultiplier = $bufferMultiplier ?? 0.0;
if ($bufferMultiplier < 0) {
throw new InvalidArgumentException('Buffer multiplier must be non-negative');
}
2025-12-29 23:32:05 +01:00
// Set allocation_value to null for unlimited buckets
2025-12-31 02:34:30 +01:00
if ($allocationType === BucketAllocationTypeEnum::UNLIMITED) {
2025-12-29 23:32:05 +01:00
$allocationValue = null;
}
// Buffer only applies to fixed_limit buckets
if ($allocationType !== BucketAllocationTypeEnum::FIXED_LIMIT) {
$bufferMultiplier = 0.0;
}
return DB::transaction(function () use ($scenario, $name, $allocationType, $allocationValue, $priority, $type, $bufferMultiplier) {
2025-12-29 23:32:05 +01:00
// Determine priority (append to end if not specified)
if ($priority === null) {
$maxPriority = $scenario->buckets()->max('priority') ?? 0;
$priority = $maxPriority + 1;
} else {
// Validate priority is positive
if ($priority < 1) {
throw new InvalidArgumentException('Priority must be at least 1');
2025-12-29 23:32:05 +01:00
}
// Check if priority already exists and shift others if needed
$existingBucket = $scenario->buckets()->where('priority', $priority)->first();
if ($existingBucket) {
// Shift priorities in reverse order to avoid unique constraint violations
// (SQLite checks constraints per-row during bulk updates)
2025-12-29 23:32:05 +01:00
$scenario->buckets()
->where('priority', '>=', $priority)
->orderByDesc('priority')
->each(function ($bucket) {
$bucket->increment('priority');
});
2025-12-29 23:32:05 +01:00
}
}
// Create the bucket
return $scenario->buckets()->create([
'name' => $name,
'type' => $type,
2025-12-29 23:32:05 +01:00
'priority' => $priority,
'sort_order' => $priority, // Start with sort_order matching priority
'allocation_type' => $allocationType,
'allocation_value' => $allocationValue,
'buffer_multiplier' => $bufferMultiplier,
2025-12-29 23:32:05 +01:00
]);
});
}
/**
* Validate that bucket type and allocation type are compatible.
*/
private function validateTypeConstraints(BucketTypeEnum $type, BucketAllocationTypeEnum $allocationType): void
{
$allowedTypes = $type->getAllowedAllocationTypes();
if (! in_array($allocationType, $allowedTypes, true)) {
$allowed = implode(', ', array_map(fn ($t) => $t->value, $allowedTypes));
throw new InvalidArgumentException("Invalid allocation type for {$type->value} bucket. Allowed: {$allowed}");
}
}
2025-12-29 23:32:05 +01:00
/**
* Validate allocation value based on allocation type.
*/
2025-12-31 02:34:30 +01:00
private function validateAllocationValue(BucketAllocationTypeEnum $allocationType, ?float $allocationValue): void
2025-12-29 23:32:05 +01:00
{
switch ($allocationType) {
2025-12-31 02:34:30 +01:00
case BucketAllocationTypeEnum::FIXED_LIMIT:
2025-12-29 23:32:05 +01:00
if ($allocationValue === null) {
throw new InvalidArgumentException('Fixed limit buckets require an allocation value');
}
if ($allocationValue < 0) {
throw new InvalidArgumentException('Fixed limit allocation value must be non-negative');
}
break;
2025-12-31 02:34:30 +01:00
case BucketAllocationTypeEnum::PERCENTAGE:
2025-12-29 23:32:05 +01:00
if ($allocationValue === null) {
throw new InvalidArgumentException('Percentage buckets require an allocation value');
}
if ($allocationValue < 1 || $allocationValue > 10000) {
throw new InvalidArgumentException('Percentage allocation value must be between 1 and 10000');
2025-12-29 23:32:05 +01:00
}
break;
2025-12-31 02:34:30 +01:00
case BucketAllocationTypeEnum::UNLIMITED:
2025-12-29 23:32:05 +01:00
// Unlimited buckets should not have an allocation value
// We'll set it to null in the main method regardless
break;
}
}
2025-12-31 01:33:44 +01:00
/**
* Create default buckets for a scenario.
*/
public function createDefaultBuckets(Scenario $scenario): array
{
$buckets = [];
// Monthly Expenses - Need, fixed limit, priority 1
2025-12-31 01:33:44 +01:00
$buckets[] = $this->execute(
$scenario,
'Monthly Expenses',
2025-12-31 02:34:30 +01:00
BucketAllocationTypeEnum::FIXED_LIMIT,
BucketTypeEnum::NEED,
2025-12-31 01:33:44 +01:00
0,
1
);
// Emergency Fund - Need, fixed limit, priority 2
2025-12-31 01:33:44 +01:00
$buckets[] = $this->execute(
$scenario,
'Emergency Fund',
2025-12-31 02:34:30 +01:00
BucketAllocationTypeEnum::FIXED_LIMIT,
BucketTypeEnum::NEED,
2025-12-31 01:33:44 +01:00
0,
2
);
// Overflow - Overflow, unlimited, priority 3
2025-12-31 01:33:44 +01:00
$buckets[] = $this->execute(
$scenario,
'Overflow',
2025-12-31 02:34:30 +01:00
BucketAllocationTypeEnum::UNLIMITED,
BucketTypeEnum::OVERFLOW,
2025-12-31 01:33:44 +01:00
null,
3
);
2025-12-31 01:33:44 +01:00
return $buckets;
}
}