5 - Add buffer multiplier support to Bucket model with effective capacity

This commit is contained in:
myrmidex 2026-03-20 00:25:44 +01:00
parent b4903cf3cf
commit 772f4c1c5a
3 changed files with 125 additions and 2 deletions

View file

@ -22,6 +22,7 @@
* @property BucketAllocationTypeEnum $allocation_type
* @property float $starting_amount
* @property float $allocation_value
* @property float $buffer_multiplier
*
* @method static BucketFactory factory()
*/
@ -38,6 +39,7 @@ class Bucket extends Model
'sort_order',
'allocation_type',
'allocation_value',
'buffer_multiplier',
'starting_amount',
];
@ -46,6 +48,7 @@ class Bucket extends Model
'priority' => 'integer',
'sort_order' => 'integer',
'allocation_value' => 'decimal:2',
'buffer_multiplier' => 'decimal:2',
'starting_amount' => 'integer',
'allocation_type' => BucketAllocationTypeEnum::class,
];
@ -99,6 +102,21 @@ public function getCurrentBalance(): int
return $this->starting_amount + $totalDraws - $totalOutflows;
}
/**
* Get the effective capacity including buffer.
* Formula: allocation_value * (1 + buffer_multiplier)
*/
public function getEffectiveCapacity(): float
{
if ($this->allocation_type !== BucketAllocationTypeEnum::FIXED_LIMIT) {
return PHP_FLOAT_MAX;
}
$base = (float) ($this->allocation_value ?? 0);
return round($base * (1 + (float) $this->buffer_multiplier), 2);
}
/**
* Check if the bucket can accept more money (for fixed_limit buckets).
*/
@ -108,7 +126,7 @@ public function hasAvailableSpace(): bool
return true;
}
return $this->getCurrentBalance() < $this->allocation_value;
return $this->getCurrentBalance() < $this->getEffectiveCapacity();
}
/**
@ -120,7 +138,7 @@ public function getAvailableSpace(): float
return PHP_FLOAT_MAX;
}
return max(0, $this->allocation_value - $this->getCurrentBalance());
return max(0, $this->getEffectiveCapacity() - $this->getCurrentBalance());
}
/**

View file

@ -46,6 +46,7 @@ public function definition(): array
'sort_order' => $this->faker->numberBetween(0, 10),
'allocation_type' => $allocationType,
'allocation_value' => $this->getAllocationValueForType($allocationType),
'buffer_multiplier' => 0,
'starting_amount' => $this->faker->numberBetween(0, 100000), // $0 to $1000 in cents
];
}
@ -119,6 +120,16 @@ public function overflow(): Factory
]);
}
/**
* Create a bucket with a buffer multiplier.
*/
public function withBuffer(float $multiplier): Factory
{
return $this->state([
'buffer_multiplier' => $multiplier,
]);
}
/**
* Create default buckets set (Monthly Expenses, Emergency Fund, Overflow).
*/

View file

@ -79,4 +79,98 @@ public function test_current_balance_without_starting_amount_defaults_to_zero()
// starting_amount (0) + draws (30000) - outflows (10000) = 20000
$this->assertEquals(20000, $bucket->getCurrentBalance());
}
public function test_effective_capacity_with_zero_buffer_equals_allocation_value(): void
{
$scenario = Scenario::factory()->create();
$bucket = Bucket::factory()->fixedLimit(50000)->create([
'scenario_id' => $scenario->id,
'buffer_multiplier' => 0,
]);
$this->assertEquals(50000.0, $bucket->getEffectiveCapacity());
}
public function test_effective_capacity_with_full_buffer_doubles_capacity(): void
{
$scenario = Scenario::factory()->create();
$bucket = Bucket::factory()->fixedLimit(50000)->create([
'scenario_id' => $scenario->id,
'buffer_multiplier' => 1.0,
]);
$this->assertEquals(100000.0, $bucket->getEffectiveCapacity());
}
public function test_effective_capacity_with_half_buffer(): void
{
$scenario = Scenario::factory()->create();
$bucket = Bucket::factory()->fixedLimit(50000)->create([
'scenario_id' => $scenario->id,
'buffer_multiplier' => 0.5,
]);
$this->assertEquals(75000.0, $bucket->getEffectiveCapacity());
}
public function test_effective_capacity_for_percentage_returns_php_float_max(): void
{
$scenario = Scenario::factory()->create();
$bucket = Bucket::factory()->percentage(25)->create([
'scenario_id' => $scenario->id,
'buffer_multiplier' => 1.0,
]);
$this->assertEquals(PHP_FLOAT_MAX, $bucket->getEffectiveCapacity());
}
public function test_effective_capacity_for_unlimited_returns_php_float_max(): void
{
$scenario = Scenario::factory()->create();
$bucket = Bucket::factory()->unlimited()->create([
'scenario_id' => $scenario->id,
'buffer_multiplier' => 1.0,
]);
$this->assertEquals(PHP_FLOAT_MAX, $bucket->getEffectiveCapacity());
}
public function test_has_available_space_uses_effective_capacity(): void
{
$scenario = Scenario::factory()->create();
$bucket = Bucket::factory()->fixedLimit(50000)->create([
'scenario_id' => $scenario->id,
'starting_amount' => 50000,
'buffer_multiplier' => 1.0,
]);
// Balance is 50000, effective capacity is 100000 — still has space
$this->assertTrue($bucket->hasAvailableSpace());
}
public function test_has_available_space_false_when_at_effective_capacity(): void
{
$scenario = Scenario::factory()->create();
$bucket = Bucket::factory()->fixedLimit(50000)->create([
'scenario_id' => $scenario->id,
'starting_amount' => 100000,
'buffer_multiplier' => 1.0,
]);
// Balance is 100000, effective capacity is 100000 — no space
$this->assertFalse($bucket->hasAvailableSpace());
}
public function test_get_available_space_uses_effective_capacity(): void
{
$scenario = Scenario::factory()->create();
$bucket = Bucket::factory()->fixedLimit(50000)->create([
'scenario_id' => $scenario->id,
'starting_amount' => 30000,
'buffer_multiplier' => 1.0,
]);
// Effective capacity 100000 - balance 30000 = 70000 available
$this->assertEquals(70000.0, $bucket->getAvailableSpace());
}
}