buckets/app/Actions/CreateBucketAction.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;
}
}