diff --git a/app/Enums/BucketAllocationTypeEnum.php b/app/Enums/BucketAllocationTypeEnum.php index 97f42d6..7e9055b 100644 --- a/app/Enums/BucketAllocationTypeEnum.php +++ b/app/Enums/BucketAllocationTypeEnum.php @@ -25,8 +25,8 @@ public static function values(): array public function getAllocationValueRules(): array { return match ($this) { - self::FIXED_LIMIT => ['required', 'numeric', 'min:0'], - self::PERCENTAGE => ['required', 'numeric', 'min:0.01', 'max:100'], + self::FIXED_LIMIT => ['required', 'integer', 'min:0'], + self::PERCENTAGE => ['required', 'integer', 'min:1', 'max:10000'], self::UNLIMITED => ['nullable'], }; } diff --git a/app/Http/Controllers/BucketController.php b/app/Http/Controllers/BucketController.php index bcd13c2..188f81a 100644 --- a/app/Http/Controllers/BucketController.php +++ b/app/Http/Controllers/BucketController.php @@ -32,7 +32,7 @@ public function store(Request $request, Scenario $scenario): JsonResponse 'name' => 'required|string|max:255', 'type' => 'required|in:'.implode(',', BucketTypeEnum::values()), 'allocation_type' => 'required|in:'.implode(',', BucketAllocationTypeEnum::values()), - 'allocation_value' => 'nullable|numeric', + 'allocation_value' => 'nullable|integer', 'buffer_multiplier' => 'sometimes|numeric|min:0', 'priority' => 'nullable|integer|min:1', ]); @@ -75,7 +75,7 @@ public function update(Request $request, Bucket $bucket): JsonResponse 'name' => 'sometimes|required|string|max:255', 'type' => 'sometimes|required|in:'.implode(',', BucketTypeEnum::values()), 'allocation_type' => 'sometimes|required|in:'.implode(',', BucketAllocationTypeEnum::values()), - 'allocation_value' => 'sometimes|nullable|numeric', + 'allocation_value' => 'sometimes|nullable|integer', 'buffer_multiplier' => 'sometimes|numeric|min:0', 'starting_amount' => 'sometimes|integer|min:0', 'priority' => 'sometimes|nullable|integer|min:1', @@ -221,6 +221,8 @@ private function validateBucketTypeConstraints( /** * Format bucket data for JSON response. + * All amounts in storage units (cents for currency, basis points for percentages). + * Frontend handles conversion to display units. */ private function formatBucketResponse(Bucket $bucket): array { @@ -235,11 +237,11 @@ private function formatBucketResponse(Bucket $bucket): array 'allocation_value' => $bucket->allocation_value, 'allocation_type_label' => $bucket->getAllocationTypeLabel(), 'buffer_multiplier' => (float) $bucket->buffer_multiplier, - 'effective_capacity' => $bucket->getEffectiveCapacity(), + 'effective_capacity' => $bucket->hasFiniteCapacity() ? $bucket->getEffectiveCapacity() : null, 'starting_amount' => $bucket->starting_amount, 'current_balance' => $bucket->getCurrentBalance(), 'has_available_space' => $bucket->hasAvailableSpace(), - 'available_space' => $bucket->getAvailableSpace(), + 'available_space' => $bucket->hasFiniteCapacity() ? $bucket->getAvailableSpace() : null, ]; } diff --git a/app/Http/Controllers/ProjectionController.php b/app/Http/Controllers/ProjectionController.php index dfd5b5f..62445c1 100644 --- a/app/Http/Controllers/ProjectionController.php +++ b/app/Http/Controllers/ProjectionController.php @@ -33,9 +33,12 @@ public function calculate(CalculateProjectionRequest $request, Scenario $scenari return new ProjectionResource($projections); } + /** + * All amounts in cents. Frontend handles conversion to display units. + */ public function preview(PreviewAllocationRequest $request, Scenario $scenario): JsonResponse { - $amountInCents = (int) round($request->input('amount') * 100); + $amountInCents = (int) $request->input('amount'); $draws = $this->pipelineAllocationService->allocateInflow($scenario, $amountInCents); @@ -49,30 +52,19 @@ public function preview(PreviewAllocationRequest $request, Scenario $scenario): 'bucket_id' => $bucket->uuid, 'bucket_name' => $bucket->name, 'bucket_type' => $bucket->type->value, - 'allocated_amount' => (float) $draw->amount_currency, - 'remaining_capacity' => $this->remainingCapacity($bucket, $draw->amount), + 'allocated_amount' => $draw->amount, + 'remaining_capacity' => $bucket->hasFiniteCapacity() + ? max(0, $bucket->getEffectiveCapacity() - $draw->amount) + : null, ]; })->values(); - $totalAllocatedCents = $draws->sum('amount'); + $totalAllocated = $draws->sum('amount'); return response()->json([ 'allocations' => $allocations, - 'total_allocated' => (float) round($totalAllocatedCents / 100, 2), - 'unallocated' => (float) round(($amountInCents - $totalAllocatedCents) / 100, 2), + 'total_allocated' => $totalAllocated, + 'unallocated' => $amountInCents - $totalAllocated, ]); } - - private function remainingCapacity(Bucket $bucket, int $allocatedCents): ?float - { - $capacityCents = $bucket->getEffectiveCapacity(); - - if ($capacityCents === PHP_INT_MAX) { - return null; - } - - $remainingCents = max(0, $capacityCents - $allocatedCents); - - return round($remainingCents / 100, 2); - } } diff --git a/app/Http/Requests/PreviewAllocationRequest.php b/app/Http/Requests/PreviewAllocationRequest.php index 42cbb93..91fe72f 100644 --- a/app/Http/Requests/PreviewAllocationRequest.php +++ b/app/Http/Requests/PreviewAllocationRequest.php @@ -14,7 +14,7 @@ public function authorize(): bool public function rules(): array { return [ - 'amount' => ['required', 'numeric', 'min:0.01'], + 'amount' => ['required', 'integer', 'min:1'], ]; } } diff --git a/app/Http/Resources/BucketResource.php b/app/Http/Resources/BucketResource.php index 8eec11e..7896d07 100644 --- a/app/Http/Resources/BucketResource.php +++ b/app/Http/Resources/BucketResource.php @@ -7,6 +7,10 @@ class BucketResource extends JsonResource { + /** + * All amounts in storage units (cents for currency, basis points for percentages). + * Frontend handles conversion to display units. + */ public function toArray(Request $request): array { return [ @@ -20,11 +24,11 @@ public function toArray(Request $request): array 'allocation_value' => $this->allocation_value, 'allocation_type_label' => $this->getAllocationTypeLabel(), 'buffer_multiplier' => (float) $this->buffer_multiplier, - 'effective_capacity' => $this->getEffectiveCapacity(), + 'effective_capacity' => $this->hasFiniteCapacity() ? $this->getEffectiveCapacity() : null, 'starting_amount' => $this->starting_amount, 'current_balance' => $this->getCurrentBalance(), 'has_available_space' => $this->hasAvailableSpace(), - 'available_space' => $this->getAvailableSpace(), + 'available_space' => $this->hasFiniteCapacity() ? $this->getAvailableSpace() : null, ]; } } diff --git a/app/Models/Bucket.php b/app/Models/Bucket.php index c5b1750..7476543 100644 --- a/app/Models/Bucket.php +++ b/app/Models/Bucket.php @@ -141,6 +141,14 @@ public function getAvailableSpace(): int return max(0, $this->getEffectiveCapacity() - $this->getCurrentBalance()); } + /** + * Whether this bucket has a finite capacity (fixed_limit only). + */ + public function hasFiniteCapacity(): bool + { + return $this->allocation_type === BucketAllocationTypeEnum::FIXED_LIMIT; + } + /** * Get display label for allocation type. */ diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 0874c99..03a52bc 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -198,6 +198,12 @@ parameters: count: 1 path: app/Http/Resources/BucketResource.php + - + message: '#^Call to an undefined method App\\Http\\Resources\\BucketResource\:\:hasFiniteCapacity\(\)\.$#' + identifier: method.notFound + count: 2 + path: app/Http/Resources/BucketResource.php + - message: '#^Access to an undefined property App\\Http\\Resources\\DrawResource\:\:\$amount_currency\.$#' identifier: property.notFound