From 4554f4e417291ad8bc04fe7151e8236e072b622b Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sat, 21 Mar 2026 16:54:11 +0100 Subject: [PATCH] 16 - Migrate allocation_value to integer and update model --- app/Actions/CreateBucketAction.php | 4 ++-- app/Http/Controllers/ProjectionController.php | 7 ++---- app/Models/Bucket.php | 22 +++++++++---------- .../Projection/PipelineAllocationService.php | 9 ++++---- database/factories/BucketFactory.php | 22 +++++++++++-------- ...2025_12_29_205724_create_buckets_table.php | 12 +++++----- phpstan-baseline.neon | 10 --------- 7 files changed, 37 insertions(+), 49 deletions(-) diff --git a/app/Actions/CreateBucketAction.php b/app/Actions/CreateBucketAction.php index 7e0c0c0..cdc62cb 100644 --- a/app/Actions/CreateBucketAction.php +++ b/app/Actions/CreateBucketAction.php @@ -116,8 +116,8 @@ private function validateAllocationValue(BucketAllocationTypeEnum $allocationTyp 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'); + if ($allocationValue < 1 || $allocationValue > 10000) { + throw new InvalidArgumentException('Percentage allocation value must be between 1 and 10000'); } break; diff --git a/app/Http/Controllers/ProjectionController.php b/app/Http/Controllers/ProjectionController.php index b081dd4..dfd5b5f 100644 --- a/app/Http/Controllers/ProjectionController.php +++ b/app/Http/Controllers/ProjectionController.php @@ -65,15 +65,12 @@ public function preview(PreviewAllocationRequest $request, Scenario $scenario): private function remainingCapacity(Bucket $bucket, int $allocatedCents): ?float { - $effectiveCapacity = $bucket->getEffectiveCapacity(); + $capacityCents = $bucket->getEffectiveCapacity(); - if ($effectiveCapacity === PHP_FLOAT_MAX) { + if ($capacityCents === PHP_INT_MAX) { return null; } - // PipelineAllocationService treats getEffectiveCapacity() as cents, - // so we compute remaining in the same unit, then convert to dollars. - $capacityCents = (int) round($effectiveCapacity); $remainingCents = max(0, $capacityCents - $allocatedCents); return round($remainingCents / 100, 2); diff --git a/app/Models/Bucket.php b/app/Models/Bucket.php index b1cb91b..c5b1750 100644 --- a/app/Models/Bucket.php +++ b/app/Models/Bucket.php @@ -20,8 +20,8 @@ * @property string $name * @property int $priority * @property BucketAllocationTypeEnum $allocation_type - * @property float $starting_amount - * @property float $allocation_value + * @property int $starting_amount + * @property int|null $allocation_value * @property float $buffer_multiplier * * @method static BucketFactory factory() @@ -47,7 +47,7 @@ class Bucket extends Model 'type' => BucketTypeEnum::class, 'priority' => 'integer', 'sort_order' => 'integer', - 'allocation_value' => 'decimal:2', + 'allocation_value' => 'integer', 'buffer_multiplier' => 'decimal:2', 'starting_amount' => 'integer', 'allocation_type' => BucketAllocationTypeEnum::class, @@ -103,18 +103,18 @@ public function getCurrentBalance(): int } /** - * Get the effective capacity including buffer. + * Get the effective capacity including buffer, in cents. * Formula: allocation_value * (1 + buffer_multiplier) */ - public function getEffectiveCapacity(): float + public function getEffectiveCapacity(): int { if ($this->allocation_type !== BucketAllocationTypeEnum::FIXED_LIMIT) { - return PHP_FLOAT_MAX; + return PHP_INT_MAX; } - $base = (float) ($this->allocation_value ?? 0); + $base = $this->allocation_value ?? 0; - return round($base * (1 + (float) $this->buffer_multiplier), 2); + return (int) round($base * (1 + (float) $this->buffer_multiplier)); } /** @@ -130,12 +130,12 @@ public function hasAvailableSpace(): bool } /** - * Get available space for fixed_limit buckets. + * Get available space in cents for fixed_limit buckets. */ - public function getAvailableSpace(): float + public function getAvailableSpace(): int { if ($this->allocation_type !== BucketAllocationTypeEnum::FIXED_LIMIT) { - return PHP_FLOAT_MAX; + return PHP_INT_MAX; } return max(0, $this->getEffectiveCapacity() - $this->getCurrentBalance()); diff --git a/app/Services/Projection/PipelineAllocationService.php b/app/Services/Projection/PipelineAllocationService.php index 8540bf2..a6abb88 100644 --- a/app/Services/Projection/PipelineAllocationService.php +++ b/app/Services/Projection/PipelineAllocationService.php @@ -81,20 +81,19 @@ private function calculateBucketAllocation(Bucket $bucket, int $remainingAmount) */ private function calculateFixedAllocation(Bucket $bucket, int $remainingAmount): int { - $bucketCapacity = (int) round($bucket->getEffectiveCapacity()); - $currentBalance = $bucket->getCurrentBalance(); - $availableSpace = max(0, $bucketCapacity - $currentBalance); + $availableSpace = $bucket->getAvailableSpace(); return min($availableSpace, $remainingAmount); } /** * Calculate allocation for percentage buckets. + * allocation_value is stored in basis points (2500 = 25%). */ private function calculatePercentageAllocation(Bucket $bucket, int $remainingAmount): int { - $percentage = $bucket->allocation_value ?? 0; + $basisPoints = $bucket->allocation_value ?? 0; - return (int) round($remainingAmount * ($percentage / 100)); + return (int) round($remainingAmount * ($basisPoints / 10000)); } } diff --git a/database/factories/BucketFactory.php b/database/factories/BucketFactory.php index 22d31bb..e6f3a68 100644 --- a/database/factories/BucketFactory.php +++ b/database/factories/BucketFactory.php @@ -53,27 +53,31 @@ public function definition(): array /** * Create a fixed limit bucket. + * + * @param int|null $amountInCents Capacity in cents (e.g., 50000 = $500) */ - public function fixedLimit($amount = null): Factory + public function fixedLimit(?int $amountInCents = null): Factory { - $amount = $amount ?? $this->faker->numberBetween(500, 5000); + $amountInCents = $amountInCents ?? $this->faker->numberBetween(50000, 500000); return $this->state([ 'allocation_type' => BucketAllocationTypeEnum::FIXED_LIMIT, - 'allocation_value' => $amount, + 'allocation_value' => $amountInCents, ]); } /** * Create a percentage bucket. + * + * @param int|null $basisPoints Percentage in basis points (e.g., 2500 = 25%) */ - public function percentage($percentage = null): Factory + public function percentage(?int $basisPoints = null): Factory { - $percentage = $percentage ?? $this->faker->numberBetween(10, 50); + $basisPoints = $basisPoints ?? $this->faker->numberBetween(1000, 5000); return $this->state([ 'allocation_type' => BucketAllocationTypeEnum::PERCENTAGE, - 'allocation_value' => $percentage, + 'allocation_value' => $basisPoints, ]); } @@ -169,11 +173,11 @@ public function defaultSet(): array /** * Get allocation value based on type. */ - private function getAllocationValueForType(BucketAllocationTypeEnum $type): ?float + private function getAllocationValueForType(BucketAllocationTypeEnum $type): ?int { return match ($type) { - BucketAllocationTypeEnum::FIXED_LIMIT => $this->faker->numberBetween(100, 10000), - BucketAllocationTypeEnum::PERCENTAGE => $this->faker->numberBetween(5, 50), + BucketAllocationTypeEnum::FIXED_LIMIT => $this->faker->numberBetween(10000, 1000000), + BucketAllocationTypeEnum::PERCENTAGE => $this->faker->numberBetween(500, 5000), BucketAllocationTypeEnum::UNLIMITED => null, }; } diff --git a/database/migrations/2025_12_29_205724_create_buckets_table.php b/database/migrations/2025_12_29_205724_create_buckets_table.php index 0c90e85..57cd36d 100644 --- a/database/migrations/2025_12_29_205724_create_buckets_table.php +++ b/database/migrations/2025_12_29_205724_create_buckets_table.php @@ -11,17 +11,15 @@ public function up(): void { Schema::create('buckets', function (Blueprint $table) { $table->id(); - $table->uuid('uuid')->unique(); + $table->uuid()->unique(); $table->foreignId('scenario_id')->constrained()->onDelete('cascade'); $table->enum('type', BucketTypeEnum::values())->default(BucketTypeEnum::NEED->value); $table->string('name'); - $table->integer('priority')->comment('Lower number = higher priority, 1 = first'); - $table->integer('sort_order')->default(0)->comment('For UI display ordering'); + $table->integer('priority'); + $table->integer('sort_order')->default(0); $table->enum('allocation_type', ['fixed_limit', 'percentage', 'unlimited']); - $table->decimal('allocation_value', 10, 2)->nullable() - ->comment('Limit amount for fixed_limit, percentage for percentage type, NULL for unlimited'); - $table->unsignedBigInteger('starting_amount')->default(0) - ->comment('Initial amount in bucket in cents before any draws or outflows'); + $table->unsignedBigInteger('allocation_value')->nullable(); + $table->unsignedBigInteger('starting_amount')->default(0); $table->timestamps(); // Indexes for performance diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index b93cf01..0874c99 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -438,11 +438,6 @@ parameters: count: 1 path: app/Http/Resources/StreamResource.php - - - message: '#^Method App\\Models\\Bucket\:\:getCurrentBalance\(\) should return int but returns float\.$#' - identifier: return.type - count: 1 - path: app/Models/Bucket.php - message: '#^Property App\\Models\\Draw\:\:\$casts \(array\\) on left side of \?\? is not nullable\.$#' @@ -624,8 +619,3 @@ parameters: count: 3 path: tests/Unit/Actions/CreateBucketActionTest.php - - - message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertNull\(\) with float will always evaluate to false\.$#' - identifier: method.impossibleType - count: 2 - path: tests/Unit/Actions/CreateBucketActionTest.php