Compare commits
No commits in common. "e563816cb38b9806aa8aae2c0e72074e629cd49b" and "473b88b54e6361f1b7048ffa60c7ae1507b632bf" have entirely different histories.
e563816cb3
...
473b88b54e
8 changed files with 89 additions and 129 deletions
|
|
@ -1,29 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace DoctrineMigrations;
|
|
||||||
|
|
||||||
use Doctrine\DBAL\Schema\Schema;
|
|
||||||
use Doctrine\Migrations\AbstractMigration;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Drop the unused distribution_mode column from scenario (#61).
|
|
||||||
*/
|
|
||||||
final class Version20260627065959 extends AbstractMigration
|
|
||||||
{
|
|
||||||
public function getDescription(): string
|
|
||||||
{
|
|
||||||
return 'Drop the unused distribution_mode column from scenario (no consumer; #61).';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function up(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('ALTER TABLE scenario DROP distribution_mode');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(Schema $schema): void
|
|
||||||
{
|
|
||||||
// Assumes an empty table (the column was always empty/unused); the
|
|
||||||
// NOT NULL re-add would fail on any pre-existing populated rows.
|
|
||||||
$this->addSql('ALTER TABLE scenario ADD distribution_mode VARCHAR(255) NOT NULL');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -6,6 +6,7 @@ use ApiPlatform\Metadata\ApiResource;
|
||||||
use ApiPlatform\Metadata\Get;
|
use ApiPlatform\Metadata\Get;
|
||||||
use ApiPlatform\Metadata\GetCollection;
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
use ApiPlatform\Metadata\Post;
|
use ApiPlatform\Metadata\Post;
|
||||||
|
use App\Enum\DistributionMode;
|
||||||
use App\Repository\ScenarioRepository;
|
use App\Repository\ScenarioRepository;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
@ -32,6 +33,9 @@ class Scenario
|
||||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||||
private User $owner;
|
private User $owner;
|
||||||
|
|
||||||
|
#[ORM\Column(enumType: DistributionMode::class)]
|
||||||
|
private DistributionMode $distributionMode = DistributionMode::EVEN;
|
||||||
|
|
||||||
#[ORM\Column(length: 255)]
|
#[ORM\Column(length: 255)]
|
||||||
#[Assert\NotBlank]
|
#[Assert\NotBlank]
|
||||||
private string $name;
|
private string $name;
|
||||||
|
|
@ -54,6 +58,11 @@ class Scenario
|
||||||
return $this->owner;
|
return $this->owner;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getDistributionMode(): DistributionMode
|
||||||
|
{
|
||||||
|
return $this->distributionMode;
|
||||||
|
}
|
||||||
|
|
||||||
public function getName(): string
|
public function getName(): string
|
||||||
{
|
{
|
||||||
return $this->name;
|
return $this->name;
|
||||||
|
|
@ -71,6 +80,13 @@ class Scenario
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setDistributionMode(DistributionMode $distributionMode): static
|
||||||
|
{
|
||||||
|
$this->distributionMode = $distributionMode;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function setName(string $name): static
|
public function setName(string $name): static
|
||||||
{
|
{
|
||||||
$this->name = $name;
|
$this->name = $name;
|
||||||
|
|
|
||||||
9
src/Enum/DistributionMode.php
Normal file
9
src/Enum/DistributionMode.php
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enum;
|
||||||
|
|
||||||
|
enum DistributionMode: string
|
||||||
|
{
|
||||||
|
case EVEN = 'even';
|
||||||
|
case PRIORITY = 'priority';
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Tests\Entity;
|
namespace App\Tests\Entity;
|
||||||
|
|
||||||
use App\Entity\Scenario;
|
use App\Entity\Scenario;
|
||||||
|
use App\Enum\DistributionMode;
|
||||||
use App\Tests\Concerns\CreatesUsers;
|
use App\Tests\Concerns\CreatesUsers;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||||
|
|
@ -41,6 +42,58 @@ final class ScenarioPersistenceTest extends KernelTestCase
|
||||||
self::assertSame('Household Budget', $reloaded->getName());
|
self::assertSame('Household Budget', $reloaded->getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testDistributionModeDefaultsToEvenWhenNotExplicitlySet(): void
|
||||||
|
{
|
||||||
|
$scenario = new Scenario();
|
||||||
|
$scenario->setName('Default Mode Scenario');
|
||||||
|
$scenario->setOwner($this->persistOwner());
|
||||||
|
|
||||||
|
$this->em->persist($scenario);
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
$id = $scenario->getId();
|
||||||
|
|
||||||
|
$this->em->clear();
|
||||||
|
|
||||||
|
$reloaded = $this->em->find(Scenario::class, $id);
|
||||||
|
|
||||||
|
self::assertNotNull($reloaded);
|
||||||
|
self::assertSame(
|
||||||
|
DistributionMode::EVEN,
|
||||||
|
$reloaded->getDistributionMode(),
|
||||||
|
'distribution_mode must default to EVEN when not explicitly set.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testItRoundTripsAnExplicitlySetPriorityDistributionMode(): void
|
||||||
|
{
|
||||||
|
$scenario = new Scenario();
|
||||||
|
$scenario->setName('Priority Mode Scenario');
|
||||||
|
$scenario->setDistributionMode(DistributionMode::PRIORITY);
|
||||||
|
$scenario->setOwner($this->persistOwner());
|
||||||
|
|
||||||
|
$this->em->persist($scenario);
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
$id = $scenario->getId();
|
||||||
|
|
||||||
|
$this->em->clear();
|
||||||
|
|
||||||
|
$reloaded = $this->em->find(Scenario::class, $id);
|
||||||
|
|
||||||
|
self::assertNotNull($reloaded);
|
||||||
|
self::assertInstanceOf(
|
||||||
|
DistributionMode::class,
|
||||||
|
$reloaded->getDistributionMode(),
|
||||||
|
'distribution_mode must hydrate as a DistributionMode enum instance, not a raw string.',
|
||||||
|
);
|
||||||
|
self::assertSame(
|
||||||
|
DistributionMode::PRIORITY,
|
||||||
|
$reloaded->getDistributionMode(),
|
||||||
|
'An explicitly set PRIORITY distribution_mode must survive a flush + clear + reload round-trip.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function testDescriptionRoundTripsWhenSet(): void
|
public function testDescriptionRoundTripsWhenSet(): void
|
||||||
{
|
{
|
||||||
$scenario = new Scenario();
|
$scenario = new Scenario();
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Tests\Enum;
|
namespace App\Tests\Enum;
|
||||||
|
|
||||||
use App\Enum\BucketAllocationType;
|
use App\Enum\BucketAllocationType;
|
||||||
|
use App\Enum\DistributionMode;
|
||||||
use App\Enum\StreamFrequency;
|
use App\Enum\StreamFrequency;
|
||||||
use App\Enum\StreamType;
|
use App\Enum\StreamType;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
@ -21,6 +22,9 @@ final class RemainingEnumsTest extends TestCase
|
||||||
|
|
||||||
$this->assertSame('income', StreamType::INCOME->value);
|
$this->assertSame('income', StreamType::INCOME->value);
|
||||||
$this->assertSame('expense', StreamType::EXPENSE->value);
|
$this->assertSame('expense', StreamType::EXPENSE->value);
|
||||||
|
|
||||||
|
$this->assertSame('even', DistributionMode::EVEN->value);
|
||||||
|
$this->assertSame('priority', DistributionMode::PRIORITY->value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testBucketAllocationTypeBackingValues(): void
|
public function testBucketAllocationTypeBackingValues(): void
|
||||||
|
|
|
||||||
|
|
@ -212,103 +212,6 @@ final class ScenarioApiTest extends WebTestCase
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testPostThenGetOnReturnedIriRoundTripsForTheOwner(): void
|
|
||||||
{
|
|
||||||
$this->login();
|
|
||||||
|
|
||||||
$this->client->request(
|
|
||||||
'POST',
|
|
||||||
'/api/scenarios',
|
|
||||||
server: [
|
|
||||||
'CONTENT_TYPE' => 'application/ld+json',
|
|
||||||
'HTTP_ACCEPT' => 'application/ld+json',
|
|
||||||
],
|
|
||||||
content: json_encode(['name' => 'New Scenario From UI']),
|
|
||||||
);
|
|
||||||
|
|
||||||
$created = json_decode((string) $this->client->getResponse()->getContent(), true);
|
|
||||||
|
|
||||||
self::assertIsArray($created, 'The created resource response must be a JSON-LD object.');
|
|
||||||
self::assertArrayHasKey(
|
|
||||||
'@id',
|
|
||||||
$created,
|
|
||||||
'POST must return the created scenario IRI so the SPA can redirect to its show page.',
|
|
||||||
);
|
|
||||||
|
|
||||||
$iri = $created['@id'];
|
|
||||||
|
|
||||||
$this->client->request('GET', $iri, server: [
|
|
||||||
'HTTP_ACCEPT' => 'application/ld+json',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response = $this->client->getResponse();
|
|
||||||
|
|
||||||
self::assertSame(
|
|
||||||
200,
|
|
||||||
$response->getStatusCode(),
|
|
||||||
'A GET on the just-created IRI by the same owner must return 200 — the create->redirect->show flow must never land on an owner-scope 404.',
|
|
||||||
);
|
|
||||||
|
|
||||||
$body = json_decode((string) $response->getContent(), true);
|
|
||||||
|
|
||||||
self::assertIsArray($body, 'The item response must be a JSON-LD object.');
|
|
||||||
self::assertSame(
|
|
||||||
$iri,
|
|
||||||
$body['@id'] ?? null,
|
|
||||||
'The show page must resolve the same IRI the create response returned.',
|
|
||||||
);
|
|
||||||
self::assertSame('New Scenario From UI', $body['name'] ?? null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testPostWithDescriptionPersistsAndEchoesIt(): void
|
|
||||||
{
|
|
||||||
$this->login();
|
|
||||||
|
|
||||||
$this->client->request(
|
|
||||||
'POST',
|
|
||||||
'/api/scenarios',
|
|
||||||
server: [
|
|
||||||
'CONTENT_TYPE' => 'application/ld+json',
|
|
||||||
'HTTP_ACCEPT' => 'application/ld+json',
|
|
||||||
],
|
|
||||||
content: json_encode([
|
|
||||||
'name' => 'Documented Fund',
|
|
||||||
'description' => 'Money set aside for the kitchen renovation.',
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
$response = $this->client->getResponse();
|
|
||||||
|
|
||||||
self::assertSame(
|
|
||||||
201,
|
|
||||||
$response->getStatusCode(),
|
|
||||||
'POST /api/scenarios with name + optional description must return 201 Created.',
|
|
||||||
);
|
|
||||||
|
|
||||||
$body = json_decode((string) $response->getContent(), true);
|
|
||||||
|
|
||||||
self::assertIsArray($body, 'The created resource response must be a JSON-LD object.');
|
|
||||||
self::assertSame(
|
|
||||||
'Money set aside for the kitchen renovation.',
|
|
||||||
$body['description'] ?? null,
|
|
||||||
'The create response must echo the supplied optional description back to the SPA.',
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->em->clear();
|
|
||||||
|
|
||||||
$persisted = $this->em->getRepository(Scenario::class)->findOneBy(['name' => 'Documented Fund']);
|
|
||||||
|
|
||||||
self::assertNotNull(
|
|
||||||
$persisted,
|
|
||||||
'A successful POST with a description must persist the new Scenario.',
|
|
||||||
);
|
|
||||||
self::assertSame(
|
|
||||||
'Money set aside for the kitchen renovation.',
|
|
||||||
$persisted->getDescription(),
|
|
||||||
'The optional description must survive persistence.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testPostWithMissingNameIsRejectedAsUnprocessable(): void
|
public function testPostWithMissingNameIsRejectedAsUnprocessable(): void
|
||||||
{
|
{
|
||||||
$this->login();
|
$this->login();
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ use App\Entity\Bucket;
|
||||||
use App\Entity\Scenario;
|
use App\Entity\Scenario;
|
||||||
use App\Enum\BucketAllocationType;
|
use App\Enum\BucketAllocationType;
|
||||||
use App\Enum\BucketType;
|
use App\Enum\BucketType;
|
||||||
|
use App\Enum\DistributionMode;
|
||||||
use App\Service\Allocation\AllocateIncome;
|
use App\Service\Allocation\AllocateIncome;
|
||||||
use App\Service\Allocation\Result;
|
use App\Service\Allocation\Result;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
|
@ -13,7 +14,7 @@ use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for the redesigned allocation engine — single strategy, no distribution
|
* Tests for the redesigned allocation engine — single strategy, no distribution
|
||||||
* modes. `preview()` does not accept any distribution-mode parameter —
|
* modes. `preview()` no longer accepts a `DistributionMode` parameter at all —
|
||||||
* Model C's even-split-on-tie behaviour is intrinsic to the engine, not a mode.
|
* Model C's even-split-on-tie behaviour is intrinsic to the engine, not a mode.
|
||||||
*
|
*
|
||||||
* THE MODEL (type-phased macro-order, priority tiers within each phase):
|
* THE MODEL (type-phased macro-order, priority tiers within each phase):
|
||||||
|
|
@ -475,7 +476,8 @@ final class AllocateIncomeTest extends TestCase
|
||||||
private function makeScenario(): Scenario
|
private function makeScenario(): Scenario
|
||||||
{
|
{
|
||||||
return (new Scenario())
|
return (new Scenario())
|
||||||
->setName('Household Budget');
|
->setName('Household Budget')
|
||||||
|
->setDistributionMode(DistributionMode::PRIORITY);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function makeFixedLimitBucket(
|
private function makeFixedLimitBucket(
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ use App\Entity\Bucket;
|
||||||
use App\Entity\Scenario;
|
use App\Entity\Scenario;
|
||||||
use App\Enum\BucketAllocationType;
|
use App\Enum\BucketAllocationType;
|
||||||
use App\Enum\BucketType;
|
use App\Enum\BucketType;
|
||||||
|
use App\Enum\DistributionMode;
|
||||||
use App\Service\Allocation\Result;
|
use App\Service\Allocation\Result;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
|
@ -77,7 +78,8 @@ final class ResultTest extends TestCase
|
||||||
private function makeScenario(): Scenario
|
private function makeScenario(): Scenario
|
||||||
{
|
{
|
||||||
return (new Scenario())
|
return (new Scenario())
|
||||||
->setName('Household Budget');
|
->setName('Household Budget')
|
||||||
|
->setDistributionMode(DistributionMode::PRIORITY);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function makeBucket(Scenario $scenario, int $priority, string $name): Bucket
|
private function makeBucket(Scenario $scenario, int $priority, string $name): Bucket
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue