5 - Add buffer multiplier support to Bucket model with effective capacity
This commit is contained in:
parent
b4903cf3cf
commit
772f4c1c5a
3 changed files with 125 additions and 2 deletions
|
|
@ -22,6 +22,7 @@
|
||||||
* @property BucketAllocationTypeEnum $allocation_type
|
* @property BucketAllocationTypeEnum $allocation_type
|
||||||
* @property float $starting_amount
|
* @property float $starting_amount
|
||||||
* @property float $allocation_value
|
* @property float $allocation_value
|
||||||
|
* @property float $buffer_multiplier
|
||||||
*
|
*
|
||||||
* @method static BucketFactory factory()
|
* @method static BucketFactory factory()
|
||||||
*/
|
*/
|
||||||
|
|
@ -38,6 +39,7 @@ class Bucket extends Model
|
||||||
'sort_order',
|
'sort_order',
|
||||||
'allocation_type',
|
'allocation_type',
|
||||||
'allocation_value',
|
'allocation_value',
|
||||||
|
'buffer_multiplier',
|
||||||
'starting_amount',
|
'starting_amount',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -46,6 +48,7 @@ class Bucket extends Model
|
||||||
'priority' => 'integer',
|
'priority' => 'integer',
|
||||||
'sort_order' => 'integer',
|
'sort_order' => 'integer',
|
||||||
'allocation_value' => 'decimal:2',
|
'allocation_value' => 'decimal:2',
|
||||||
|
'buffer_multiplier' => 'decimal:2',
|
||||||
'starting_amount' => 'integer',
|
'starting_amount' => 'integer',
|
||||||
'allocation_type' => BucketAllocationTypeEnum::class,
|
'allocation_type' => BucketAllocationTypeEnum::class,
|
||||||
];
|
];
|
||||||
|
|
@ -99,6 +102,21 @@ public function getCurrentBalance(): int
|
||||||
return $this->starting_amount + $totalDraws - $totalOutflows;
|
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).
|
* Check if the bucket can accept more money (for fixed_limit buckets).
|
||||||
*/
|
*/
|
||||||
|
|
@ -108,7 +126,7 @@ public function hasAvailableSpace(): bool
|
||||||
return true;
|
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 PHP_FLOAT_MAX;
|
||||||
}
|
}
|
||||||
|
|
||||||
return max(0, $this->allocation_value - $this->getCurrentBalance());
|
return max(0, $this->getEffectiveCapacity() - $this->getCurrentBalance());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ public function definition(): array
|
||||||
'sort_order' => $this->faker->numberBetween(0, 10),
|
'sort_order' => $this->faker->numberBetween(0, 10),
|
||||||
'allocation_type' => $allocationType,
|
'allocation_type' => $allocationType,
|
||||||
'allocation_value' => $this->getAllocationValueForType($allocationType),
|
'allocation_value' => $this->getAllocationValueForType($allocationType),
|
||||||
|
'buffer_multiplier' => 0,
|
||||||
'starting_amount' => $this->faker->numberBetween(0, 100000), // $0 to $1000 in cents
|
'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).
|
* Create default buckets set (Monthly Expenses, Emergency Fund, Overflow).
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -79,4 +79,98 @@ public function test_current_balance_without_starting_amount_defaults_to_zero()
|
||||||
// starting_amount (0) + draws (30000) - outflows (10000) = 20000
|
// starting_amount (0) + draws (30000) - outflows (10000) = 20000
|
||||||
$this->assertEquals(20000, $bucket->getCurrentBalance());
|
$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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue