157 lines
5.7 KiB
PHP
157 lines
5.7 KiB
PHP
<?php
|
|
|
|
namespace App\Actions;
|
|
|
|
use App\Enums\BucketAllocationTypeEnum;
|
|
use App\Enums\BucketTypeEnum;
|
|
use App\Models\Bucket;
|
|
use App\Models\Scenario;
|
|
use Illuminate\Support\Facades\DB;
|
|
use InvalidArgumentException;
|
|
|
|
class CreateBucketAction
|
|
{
|
|
public function execute(
|
|
Scenario $scenario,
|
|
string $name,
|
|
BucketAllocationTypeEnum $allocationType,
|
|
BucketTypeEnum $type = BucketTypeEnum::NEED,
|
|
?float $allocationValue = null,
|
|
?int $priority = null,
|
|
): 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');
|
|
}
|
|
|
|
// Validate allocation value based on type
|
|
$this->validateAllocationValue($allocationType, $allocationValue);
|
|
|
|
// Set allocation_value to null for unlimited buckets
|
|
if ($allocationType === BucketAllocationTypeEnum::UNLIMITED) {
|
|
$allocationValue = null;
|
|
}
|
|
|
|
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;
|
|
$priority = $maxPriority + 1;
|
|
} else {
|
|
// Validate priority is positive
|
|
if ($priority < 1) {
|
|
throw new InvalidArgumentException('Priority must be at least 1');
|
|
}
|
|
|
|
// 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)
|
|
$scenario->buckets()
|
|
->where('priority', '>=', $priority)
|
|
->orderByDesc('priority')
|
|
->each(function ($bucket) {
|
|
$bucket->increment('priority');
|
|
});
|
|
}
|
|
}
|
|
|
|
// 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,
|
|
'allocation_value' => $allocationValue,
|
|
]);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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}");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate allocation value based on allocation type.
|
|
*/
|
|
private function validateAllocationValue(BucketAllocationTypeEnum $allocationType, ?float $allocationValue): void
|
|
{
|
|
switch ($allocationType) {
|
|
case BucketAllocationTypeEnum::FIXED_LIMIT:
|
|
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;
|
|
|
|
case BucketAllocationTypeEnum::PERCENTAGE:
|
|
if ($allocationValue === null) {
|
|
throw new InvalidArgumentException('Percentage buckets require an allocation value');
|
|
}
|
|
if ($allocationValue < 0.01 || $allocationValue > 100) {
|
|
throw new InvalidArgumentException('Percentage allocation value must be between 0.01 and 100');
|
|
}
|
|
break;
|
|
|
|
case BucketAllocationTypeEnum::UNLIMITED:
|
|
// Unlimited buckets should not have an allocation value
|
|
// We'll set it to null in the main method regardless
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create default buckets for a scenario.
|
|
*/
|
|
public function createDefaultBuckets(Scenario $scenario): array
|
|
{
|
|
$buckets = [];
|
|
|
|
// Monthly Expenses - Need, fixed limit, priority 1
|
|
$buckets[] = $this->execute(
|
|
$scenario,
|
|
'Monthly Expenses',
|
|
BucketAllocationTypeEnum::FIXED_LIMIT,
|
|
BucketTypeEnum::NEED,
|
|
0,
|
|
1
|
|
);
|
|
|
|
// Emergency Fund - Need, fixed limit, priority 2
|
|
$buckets[] = $this->execute(
|
|
$scenario,
|
|
'Emergency Fund',
|
|
BucketAllocationTypeEnum::FIXED_LIMIT,
|
|
BucketTypeEnum::NEED,
|
|
0,
|
|
2
|
|
);
|
|
|
|
// Overflow - Overflow, unlimited, priority 3
|
|
$buckets[] = $this->execute(
|
|
$scenario,
|
|
'Overflow',
|
|
BucketAllocationTypeEnum::UNLIMITED,
|
|
BucketTypeEnum::OVERFLOW,
|
|
null,
|
|
3
|
|
);
|
|
|
|
return $buckets;
|
|
}
|
|
}
|