427 lines
14 KiB
PHP
427 lines
14 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\Actions;
|
|
|
|
use App\Actions\CreateBucketAction;
|
|
use App\Enums\BucketAllocationTypeEnum;
|
|
use App\Enums\BucketTypeEnum;
|
|
use App\Models\Bucket;
|
|
use App\Models\Scenario;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use InvalidArgumentException;
|
|
use Tests\TestCase;
|
|
|
|
class CreateBucketActionTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private CreateBucketAction $action;
|
|
|
|
private Scenario $scenario;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->action = new CreateBucketAction;
|
|
$this->scenario = Scenario::factory()->create();
|
|
}
|
|
|
|
public function test_can_create_fixed_limit_bucket(): void
|
|
{
|
|
$bucket = $this->action->execute(
|
|
$this->scenario,
|
|
'Test Bucket',
|
|
BucketAllocationTypeEnum::FIXED_LIMIT,
|
|
allocationValue: 100000
|
|
);
|
|
|
|
$this->assertInstanceOf(Bucket::class, $bucket);
|
|
$this->assertEquals('Test Bucket', $bucket->name);
|
|
$this->assertEquals(BucketTypeEnum::NEED, $bucket->type);
|
|
$this->assertEquals(BucketAllocationTypeEnum::FIXED_LIMIT, $bucket->allocation_type);
|
|
$this->assertEquals(100000, $bucket->allocation_value);
|
|
$this->assertEquals(1, $bucket->priority);
|
|
$this->assertEquals(1, $bucket->sort_order);
|
|
$this->assertEquals($this->scenario->id, $bucket->scenario_id);
|
|
}
|
|
|
|
public function test_can_create_percentage_bucket(): void
|
|
{
|
|
$bucket = $this->action->execute(
|
|
$this->scenario,
|
|
'Percentage Bucket',
|
|
BucketAllocationTypeEnum::PERCENTAGE,
|
|
allocationValue: 2550
|
|
);
|
|
|
|
$this->assertEquals(BucketAllocationTypeEnum::PERCENTAGE, $bucket->allocation_type);
|
|
$this->assertEquals(2550, $bucket->allocation_value);
|
|
}
|
|
|
|
public function test_can_create_unlimited_overflow_bucket(): void
|
|
{
|
|
$bucket = $this->action->execute(
|
|
$this->scenario,
|
|
'Overflow Bucket',
|
|
BucketAllocationTypeEnum::UNLIMITED,
|
|
BucketTypeEnum::OVERFLOW
|
|
);
|
|
|
|
$this->assertEquals(BucketTypeEnum::OVERFLOW, $bucket->type);
|
|
$this->assertEquals(BucketAllocationTypeEnum::UNLIMITED, $bucket->allocation_type);
|
|
$this->assertNull($bucket->allocation_value);
|
|
}
|
|
|
|
public function test_unlimited_overflow_bucket_ignores_allocation_value(): void
|
|
{
|
|
$bucket = $this->action->execute(
|
|
$this->scenario,
|
|
'Overflow Bucket',
|
|
BucketAllocationTypeEnum::UNLIMITED,
|
|
BucketTypeEnum::OVERFLOW,
|
|
99999
|
|
);
|
|
|
|
$this->assertEquals(BucketAllocationTypeEnum::UNLIMITED, $bucket->allocation_type);
|
|
$this->assertNull($bucket->allocation_value);
|
|
}
|
|
|
|
public function test_can_create_need_bucket(): void
|
|
{
|
|
$bucket = $this->action->execute(
|
|
$this->scenario,
|
|
'Need Bucket',
|
|
BucketAllocationTypeEnum::FIXED_LIMIT,
|
|
BucketTypeEnum::NEED,
|
|
500
|
|
);
|
|
|
|
$this->assertEquals(BucketTypeEnum::NEED, $bucket->type);
|
|
}
|
|
|
|
public function test_can_create_want_bucket(): void
|
|
{
|
|
$bucket = $this->action->execute(
|
|
$this->scenario,
|
|
'Want Bucket',
|
|
BucketAllocationTypeEnum::FIXED_LIMIT,
|
|
BucketTypeEnum::WANT,
|
|
500
|
|
);
|
|
|
|
$this->assertEquals(BucketTypeEnum::WANT, $bucket->type);
|
|
}
|
|
|
|
public function test_default_type_is_need(): void
|
|
{
|
|
$bucket = $this->action->execute(
|
|
$this->scenario,
|
|
'Default Type Bucket',
|
|
BucketAllocationTypeEnum::FIXED_LIMIT,
|
|
allocationValue: 100
|
|
);
|
|
|
|
$this->assertEquals(BucketTypeEnum::NEED, $bucket->type);
|
|
}
|
|
|
|
public function test_overflow_bucket_must_use_unlimited_allocation(): void
|
|
{
|
|
$this->expectException(InvalidArgumentException::class);
|
|
$this->expectExceptionMessage('Invalid allocation type for overflow bucket. Allowed: unlimited');
|
|
|
|
$this->action->execute(
|
|
$this->scenario,
|
|
'Bad Overflow',
|
|
BucketAllocationTypeEnum::FIXED_LIMIT,
|
|
BucketTypeEnum::OVERFLOW,
|
|
100
|
|
);
|
|
}
|
|
|
|
public function test_non_overflow_bucket_cannot_use_unlimited_allocation(): void
|
|
{
|
|
$this->expectException(InvalidArgumentException::class);
|
|
$this->expectExceptionMessage('Invalid allocation type for need bucket. Allowed: fixed_limit, percentage');
|
|
|
|
$this->action->execute(
|
|
$this->scenario,
|
|
'Bad Need Bucket',
|
|
BucketAllocationTypeEnum::UNLIMITED,
|
|
BucketTypeEnum::NEED
|
|
);
|
|
}
|
|
|
|
public function test_cannot_create_second_overflow_bucket(): void
|
|
{
|
|
$this->action->execute(
|
|
$this->scenario,
|
|
'First Overflow',
|
|
BucketAllocationTypeEnum::UNLIMITED,
|
|
BucketTypeEnum::OVERFLOW
|
|
);
|
|
|
|
$this->expectException(InvalidArgumentException::class);
|
|
$this->expectExceptionMessage('A scenario can only have one overflow bucket');
|
|
|
|
$this->action->execute(
|
|
$this->scenario,
|
|
'Second Overflow',
|
|
BucketAllocationTypeEnum::UNLIMITED,
|
|
BucketTypeEnum::OVERFLOW
|
|
);
|
|
}
|
|
|
|
public function test_want_bucket_cannot_use_unlimited_allocation(): void
|
|
{
|
|
$this->expectException(InvalidArgumentException::class);
|
|
$this->expectExceptionMessage('Invalid allocation type for want bucket. Allowed: fixed_limit, percentage');
|
|
|
|
$this->action->execute(
|
|
$this->scenario,
|
|
'Bad Want Bucket',
|
|
BucketAllocationTypeEnum::UNLIMITED,
|
|
BucketTypeEnum::WANT
|
|
);
|
|
}
|
|
|
|
public function test_priority_auto_increments_when_not_specified(): void
|
|
{
|
|
$bucket1 = $this->action->execute(
|
|
$this->scenario,
|
|
'First Bucket',
|
|
BucketAllocationTypeEnum::FIXED_LIMIT,
|
|
allocationValue: 100
|
|
);
|
|
|
|
$bucket2 = $this->action->execute(
|
|
$this->scenario,
|
|
'Second Bucket',
|
|
BucketAllocationTypeEnum::FIXED_LIMIT,
|
|
allocationValue: 200
|
|
);
|
|
|
|
$this->assertEquals(1, $bucket1->priority);
|
|
$this->assertEquals(2, $bucket2->priority);
|
|
}
|
|
|
|
public function test_can_specify_custom_priority(): void
|
|
{
|
|
$bucket = $this->action->execute(
|
|
$this->scenario,
|
|
'Priority Bucket',
|
|
BucketAllocationTypeEnum::FIXED_LIMIT,
|
|
allocationValue: 100,
|
|
priority: 5
|
|
);
|
|
|
|
$this->assertEquals(5, $bucket->priority);
|
|
}
|
|
|
|
public function test_existing_priorities_are_shifted_when_inserting(): void
|
|
{
|
|
// Create initial buckets
|
|
$bucket1 = $this->action->execute($this->scenario, 'Bucket 1', BucketAllocationTypeEnum::FIXED_LIMIT, allocationValue: 100, priority: 1);
|
|
$bucket2 = $this->action->execute($this->scenario, 'Bucket 2', BucketAllocationTypeEnum::FIXED_LIMIT, allocationValue: 200, priority: 2);
|
|
$bucket3 = $this->action->execute($this->scenario, 'Bucket 3', BucketAllocationTypeEnum::FIXED_LIMIT, allocationValue: 300, priority: 3);
|
|
|
|
// Insert a bucket at priority 2
|
|
$newBucket = $this->action->execute($this->scenario, 'New Bucket', BucketAllocationTypeEnum::FIXED_LIMIT, allocationValue: 150, priority: 2);
|
|
|
|
// Refresh models from database
|
|
$bucket1->refresh();
|
|
$bucket2->refresh();
|
|
$bucket3->refresh();
|
|
|
|
// Check that priorities were shifted correctly
|
|
$this->assertEquals(1, $bucket1->priority);
|
|
$this->assertEquals(2, $newBucket->priority);
|
|
$this->assertEquals(3, $bucket2->priority); // Shifted from 2 to 3
|
|
$this->assertEquals(4, $bucket3->priority); // Shifted from 3 to 4
|
|
}
|
|
|
|
public function test_throws_exception_for_fixed_limit_without_allocation_value(): void
|
|
{
|
|
$this->expectException(InvalidArgumentException::class);
|
|
$this->expectExceptionMessage('Fixed limit buckets require an allocation value');
|
|
|
|
$this->action->execute(
|
|
$this->scenario,
|
|
'Test Bucket',
|
|
BucketAllocationTypeEnum::FIXED_LIMIT
|
|
);
|
|
}
|
|
|
|
public function test_throws_exception_for_negative_fixed_limit_value(): void
|
|
{
|
|
$this->expectException(InvalidArgumentException::class);
|
|
$this->expectExceptionMessage('Fixed limit allocation value must be non-negative');
|
|
|
|
$this->action->execute(
|
|
$this->scenario,
|
|
'Test Bucket',
|
|
BucketAllocationTypeEnum::FIXED_LIMIT,
|
|
allocationValue: -100
|
|
);
|
|
}
|
|
|
|
public function test_throws_exception_for_percentage_without_allocation_value(): void
|
|
{
|
|
$this->expectException(InvalidArgumentException::class);
|
|
$this->expectExceptionMessage('Percentage buckets require an allocation value');
|
|
|
|
$this->action->execute(
|
|
$this->scenario,
|
|
'Test Bucket',
|
|
BucketAllocationTypeEnum::PERCENTAGE
|
|
);
|
|
}
|
|
|
|
public function test_throws_exception_for_percentage_below_minimum(): void
|
|
{
|
|
$this->expectException(InvalidArgumentException::class);
|
|
$this->expectExceptionMessage('Percentage allocation value must be between 1 and 10000');
|
|
|
|
$this->action->execute(
|
|
$this->scenario,
|
|
'Test Bucket',
|
|
BucketAllocationTypeEnum::PERCENTAGE,
|
|
allocationValue: 0
|
|
);
|
|
}
|
|
|
|
public function test_throws_exception_for_percentage_above_maximum(): void
|
|
{
|
|
$this->expectException(InvalidArgumentException::class);
|
|
$this->expectExceptionMessage('Percentage allocation value must be between 1 and 10000');
|
|
|
|
$this->action->execute(
|
|
$this->scenario,
|
|
'Test Bucket',
|
|
BucketAllocationTypeEnum::PERCENTAGE,
|
|
allocationValue: 10001
|
|
);
|
|
}
|
|
|
|
public function test_throws_exception_for_negative_priority(): void
|
|
{
|
|
$this->expectException(InvalidArgumentException::class);
|
|
$this->expectExceptionMessage('Priority must be at least 1');
|
|
|
|
$this->action->execute(
|
|
$this->scenario,
|
|
'Test Bucket',
|
|
BucketAllocationTypeEnum::FIXED_LIMIT,
|
|
allocationValue: 100,
|
|
priority: 0
|
|
);
|
|
}
|
|
|
|
public function test_create_default_buckets(): void
|
|
{
|
|
$this->action->createDefaultBuckets($this->scenario);
|
|
|
|
$buckets = $this->scenario->buckets()->orderBy('priority')->get();
|
|
|
|
$this->assertCount(3, $buckets);
|
|
|
|
// Monthly Expenses - Need
|
|
$this->assertEquals('Monthly Expenses', $buckets[0]->name);
|
|
$this->assertEquals(BucketTypeEnum::NEED, $buckets[0]->type);
|
|
$this->assertEquals(1, $buckets[0]->priority);
|
|
$this->assertEquals(BucketAllocationTypeEnum::FIXED_LIMIT, $buckets[0]->allocation_type);
|
|
$this->assertEquals(0, $buckets[0]->allocation_value);
|
|
|
|
// Emergency Fund - Need
|
|
$this->assertEquals('Emergency Fund', $buckets[1]->name);
|
|
$this->assertEquals(BucketTypeEnum::NEED, $buckets[1]->type);
|
|
$this->assertEquals(2, $buckets[1]->priority);
|
|
$this->assertEquals(BucketAllocationTypeEnum::FIXED_LIMIT, $buckets[1]->allocation_type);
|
|
$this->assertEquals(0, $buckets[1]->allocation_value);
|
|
|
|
// Overflow
|
|
$this->assertEquals('Overflow', $buckets[2]->name);
|
|
$this->assertEquals(BucketTypeEnum::OVERFLOW, $buckets[2]->type);
|
|
$this->assertEquals(3, $buckets[2]->priority);
|
|
$this->assertEquals(BucketAllocationTypeEnum::UNLIMITED, $buckets[2]->allocation_type);
|
|
$this->assertNull($buckets[2]->allocation_value);
|
|
}
|
|
|
|
public function test_creates_buckets_in_database_transaction(): void
|
|
{
|
|
// This test ensures database consistency by creating multiple buckets
|
|
// and verifying they all exist with correct priorities
|
|
$this->action->execute($this->scenario, 'Bucket 1', BucketAllocationTypeEnum::FIXED_LIMIT, allocationValue: 100, priority: 1);
|
|
$this->action->execute($this->scenario, 'Bucket 2', BucketAllocationTypeEnum::FIXED_LIMIT, allocationValue: 200, priority: 1); // Insert at priority 1
|
|
|
|
// Both buckets should exist with correct priorities
|
|
$buckets = $this->scenario->buckets()->orderBy('priority')->get();
|
|
$this->assertCount(2, $buckets);
|
|
$this->assertEquals('Bucket 2', $buckets[0]->name); // New bucket at priority 1
|
|
$this->assertEquals('Bucket 1', $buckets[1]->name); // Original bucket shifted to priority 2
|
|
}
|
|
|
|
public function test_can_create_fixed_limit_bucket_with_buffer_multiplier(): void
|
|
{
|
|
$bucket = $this->action->execute(
|
|
$this->scenario,
|
|
'Buffered Bucket',
|
|
BucketAllocationTypeEnum::FIXED_LIMIT,
|
|
allocationValue: 50000,
|
|
bufferMultiplier: 1.5,
|
|
);
|
|
|
|
$this->assertEquals('1.50', $bucket->buffer_multiplier);
|
|
}
|
|
|
|
public function test_buffer_multiplier_defaults_to_zero(): void
|
|
{
|
|
$bucket = $this->action->execute(
|
|
$this->scenario,
|
|
'No Buffer Bucket',
|
|
BucketAllocationTypeEnum::FIXED_LIMIT,
|
|
allocationValue: 50000,
|
|
);
|
|
|
|
$this->assertEquals('0.00', $bucket->buffer_multiplier);
|
|
}
|
|
|
|
public function test_buffer_multiplier_forced_to_zero_for_percentage_bucket(): void
|
|
{
|
|
$bucket = $this->action->execute(
|
|
$this->scenario,
|
|
'Percentage Bucket',
|
|
BucketAllocationTypeEnum::PERCENTAGE,
|
|
allocationValue: 2500,
|
|
bufferMultiplier: 1.0,
|
|
);
|
|
|
|
$this->assertEquals('0.00', $bucket->buffer_multiplier);
|
|
}
|
|
|
|
public function test_buffer_multiplier_forced_to_zero_for_unlimited_bucket(): void
|
|
{
|
|
$bucket = $this->action->execute(
|
|
$this->scenario,
|
|
'Unlimited Bucket',
|
|
BucketAllocationTypeEnum::UNLIMITED,
|
|
BucketTypeEnum::OVERFLOW,
|
|
bufferMultiplier: 1.0,
|
|
);
|
|
|
|
$this->assertEquals('0.00', $bucket->buffer_multiplier);
|
|
}
|
|
|
|
public function test_throws_exception_for_negative_buffer_multiplier(): void
|
|
{
|
|
$this->expectException(InvalidArgumentException::class);
|
|
$this->expectExceptionMessage('Buffer multiplier must be non-negative');
|
|
|
|
$this->action->execute(
|
|
$this->scenario,
|
|
'Bad Buffer',
|
|
BucketAllocationTypeEnum::FIXED_LIMIT,
|
|
allocationValue: 50000,
|
|
bufferMultiplier: -0.5,
|
|
);
|
|
}
|
|
}
|