diff --git a/app/Http/Controllers/BucketController.php b/app/Http/Controllers/BucketController.php index 65fd87f..3641f86 100644 --- a/app/Http/Controllers/BucketController.php +++ b/app/Http/Controllers/BucketController.php @@ -3,6 +3,8 @@ namespace App\Http\Controllers; use App\Actions\CreateBucketAction; +use App\Enums\BucketAllocationTypeEnum; +use App\Enums\BucketTypeEnum; use App\Models\Bucket; use App\Models\Scenario; use Illuminate\Http\JsonResponse; @@ -17,21 +19,7 @@ public function index(Scenario $scenario): JsonResponse $buckets = $scenario->buckets() ->orderedBySortOrder() ->get() - ->map(function ($bucket) { - return [ - 'id' => $bucket->uuid, - 'name' => $bucket->name, - 'priority' => $bucket->priority, - 'sort_order' => $bucket->sort_order, - 'allocation_type' => $bucket->allocation_type, - 'allocation_value' => $bucket->allocation_value, - 'allocation_type_label' => $bucket->getAllocationTypeLabel(), - 'formatted_allocation_value' => $bucket->getFormattedAllocationValue(), - 'current_balance' => $bucket->getCurrentBalance(), - 'has_available_space' => $bucket->hasAvailableSpace(), - 'available_space' => $bucket->getAvailableSpace(), - ]; - }); + ->map(fn ($bucket) => $this->formatBucketResponse($bucket)); return response()->json([ 'buckets' => $buckets, @@ -42,22 +30,28 @@ public function store(Request $request, Scenario $scenario): JsonResponse { $validated = $request->validate([ 'name' => 'required|string|max:255', - 'allocation_type' => 'required|in:'.implode(',', [ - Bucket::TYPE_FIXED_LIMIT, - Bucket::TYPE_PERCENTAGE, - Bucket::TYPE_UNLIMITED, - ]), + 'type' => 'required|in:'.implode(',', BucketTypeEnum::values()), + 'allocation_type' => 'required|in:'.implode(',', BucketAllocationTypeEnum::values()), 'allocation_value' => 'nullable|numeric', 'priority' => 'nullable|integer|min:1', ]); + $type = BucketTypeEnum::from($validated['type']); + $allocationType = BucketAllocationTypeEnum::from($validated['allocation_type']); + + $constraintError = $this->validateBucketTypeConstraints($type, $allocationType, $scenario); + if ($constraintError) { + return $constraintError; + } + try { $createBucketAction = new CreateBucketAction; $bucket = $createBucketAction->execute( $scenario, $validated['name'], - $validated['allocation_type'], - $validated['allocation_value'], + $allocationType, + $type, + $validated['allocation_value'] ?? null, $validated['priority'] ?? null ); @@ -77,23 +71,28 @@ public function update(Request $request, Bucket $bucket): JsonResponse { $validated = $request->validate([ 'name' => 'required|string|max:255', - 'allocation_type' => 'required|in:'.implode(',', [ - Bucket::TYPE_FIXED_LIMIT, - Bucket::TYPE_PERCENTAGE, - Bucket::TYPE_UNLIMITED, - ]), + 'type' => 'required|in:'.implode(',', BucketTypeEnum::values()), + 'allocation_type' => 'required|in:'.implode(',', BucketAllocationTypeEnum::values()), 'allocation_value' => 'nullable|numeric', 'priority' => 'nullable|integer|min:1', ]); + $type = BucketTypeEnum::from($validated['type']); + $allocationType = BucketAllocationTypeEnum::from($validated['allocation_type']); + + $constraintError = $this->validateBucketTypeConstraints($type, $allocationType, $bucket->scenario, $bucket); + if ($constraintError) { + return $constraintError; + } + // Validate allocation_value based on allocation_type - $allocationValueRules = Bucket::allocationValueRules($validated['allocation_type']); + $allocationValueRules = Bucket::allocationValueRules($allocationType); $request->validate([ 'allocation_value' => $allocationValueRules, ]); // Set allocation_value to null for unlimited buckets - if ($validated['allocation_type'] === Bucket::TYPE_UNLIMITED) { + if ($allocationType === BucketAllocationTypeEnum::UNLIMITED) { $validated['allocation_value'] = null; } @@ -155,6 +154,47 @@ public function updatePriorities(Request $request, Scenario $scenario): JsonResp ]); } + /** + * Validate bucket type constraints (type+allocation compatibility, one overflow per scenario). + */ + private function validateBucketTypeConstraints( + BucketTypeEnum $type, + BucketAllocationTypeEnum $allocationType, + Scenario $scenario, + ?Bucket $excludeBucket = null + ): ?JsonResponse { + if ($type === BucketTypeEnum::OVERFLOW && $allocationType !== BucketAllocationTypeEnum::UNLIMITED) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => ['allocation_type' => ['Overflow buckets must use unlimited allocation type.']], + ], 422); + } + + if ($type !== BucketTypeEnum::OVERFLOW && $allocationType === BucketAllocationTypeEnum::UNLIMITED) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => ['allocation_type' => ['Only overflow buckets can use unlimited allocation type.']], + ], 422); + } + + if ($type === BucketTypeEnum::OVERFLOW) { + $query = $scenario->buckets()->where('type', BucketTypeEnum::OVERFLOW->value); + + if ($excludeBucket) { + $query->where('id', '!=', $excludeBucket->id); + } + + if ($query->exists()) { + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => ['type' => ['A scenario can only have one overflow bucket.']], + ], 422); + } + } + + return null; + } + /** * Format bucket data for JSON response. */ @@ -163,6 +203,8 @@ private function formatBucketResponse(Bucket $bucket): array return [ 'id' => $bucket->uuid, 'name' => $bucket->name, + 'type' => $bucket->type, + 'type_label' => $bucket->type->getLabel(), 'priority' => $bucket->priority, 'sort_order' => $bucket->sort_order, 'allocation_type' => $bucket->allocation_type,