feat: add a booking event history for admins
Admins get /admin/history: one line per booking created, rescheduled or released, per series created or ended, and per resource deleted with its bookings. Sortable by time, kind, resource, booked day and both people; filterable by period, kinds, who acted and resource type. Events are snapshots written inside the same transaction as the change, so a rolled-back booking leaves none and a released booking stays readable. app:booking-events:prune keeps twelve months and needs a cron entry on the server. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
d68b6b156c
commit
dcdce4d248
18 files changed
+1556
-6
No files matched your search
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Command;
|
||||
|
||||
use App\Entity\BookingEvent;
|
||||
use App\Enum\BookingEventType;
|
||||
use App\Enum\ResourceType;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
/**
|
||||
* The retention period of the event history, enforced from cron.
|
||||
*/
|
||||
class PruneBookingEventsCommandTest extends KernelTestCase
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
private CommandTester $command;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$this->em = self::getContainer()->get(EntityManagerInterface::class);
|
||||
$this->em->getConnection()->beginTransaction();
|
||||
$this->command = new CommandTester((new Application(self::$kernel))->find('app:booking-events:prune'));
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$this->em->getConnection()->rollBack();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function testEventsOlderThanAYearAreDeleted(): void
|
||||
{
|
||||
$old = $this->event('-13 months');
|
||||
$recent = $this->event('-11 months');
|
||||
|
||||
$this->command->execute([]);
|
||||
|
||||
$this->command->assertCommandIsSuccessful();
|
||||
self::assertStringContainsString('Deleted 1 booking events', $this->command->getDisplay());
|
||||
self::assertNull($this->em->find(BookingEvent::class, $old));
|
||||
self::assertNotNull($this->em->find(BookingEvent::class, $recent));
|
||||
}
|
||||
|
||||
public function testTheRetentionCanBeShortened(): void
|
||||
{
|
||||
$event = $this->event('-2 months');
|
||||
|
||||
$this->command->execute(['--months' => '1']);
|
||||
|
||||
$this->command->assertCommandIsSuccessful();
|
||||
self::assertNull($this->em->find(BookingEvent::class, $event));
|
||||
}
|
||||
|
||||
public function testANonsensicalRetentionIsRefused(): void
|
||||
{
|
||||
$event = $this->event('-2 months');
|
||||
|
||||
self::assertSame(Command::INVALID, $this->command->execute(['--months' => '0']));
|
||||
self::assertNotNull($this->em->find(BookingEvent::class, $event));
|
||||
}
|
||||
|
||||
private function event(string $when): int
|
||||
{
|
||||
$event = new BookingEvent(BookingEventType::BOOKING_CREATED, null, 'System', null, 'Tisch 1', ResourceType::DESK);
|
||||
$this->em->persist($event);
|
||||
$this->em->flush();
|
||||
|
||||
$this->em->getConnection()->executeStatement(
|
||||
'UPDATE booking_event SET occurred_at = ? WHERE id = ?',
|
||||
[(new \DateTimeImmutable($when))->format('Y-m-d H:i:s'), $event->getId()],
|
||||
);
|
||||
$this->em->clear();
|
||||
|
||||
return (int) $event->getId();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user