Tasks 07 through 13 each changed something the rest of the system might want to know about, with no contract for saying so. And task 05 shipped a powerful segment editor with no feedback on whether a clinic defined its segments right. Events - A closed list of names, because a consumer branches on the string and a one-letter typo would produce an event nobody hears and no error either - Payloads carry uuids and scalars only; non-scalars are dropped, not serialised, so a consumer always fetches fresh rather than reading a stale detached entity - record() deliberately does not flush: the event row commits with the change it describes, so a rolled-back transaction leaves no event behind. A test pins exactly that - app:events:publish drains the outbox; five failed attempts park a row with its error rather than deleting it, because a silently dropped event is a loss with no trace. app:events:prune only ever removes published rows Reports - Resource utilisation separates available, occupied and active minutes. The gap between occupied and active is what exposes a bad segment definition, and available is multiplied by capacity so a three-chair room does not read as permanently over 100% - A resource with no calendar reports utilization: null, not zero — dividing by zero means something different from being idle - Plan accuracy compares planned against actual duration per service and flags both directions: running short wastes capacity that could have been sold. Its row links straight to editing that service's segments, because a report with no route to a fix does not get read Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
66 lines
2.2 KiB
PHP
66 lines
2.2 KiB
PHP
<?php
|
|
|
|
namespace App\Shared\Event\Command;
|
|
|
|
use Doctrine\DBAL\Connection;
|
|
use Symfony\Component\Console\Attribute\AsCommand;
|
|
use Symfony\Component\Console\Command\Command;
|
|
use Symfony\Component\Console\Input\InputInterface;
|
|
use Symfony\Component\Console\Input\InputOption;
|
|
use Symfony\Component\Console\Output\OutputInterface;
|
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
|
|
|
/**
|
|
* پاکسازی رویدادهای **منتشرشدهٔ** قدیمی.
|
|
*
|
|
* ردیف منتشرنشده هرگز حذف نمیشود، حتی اگر سالخورده باشد: آن یک رویداد گمشده است و
|
|
* حذفش یعنی پاک کردن مدرکِ همان گمشدن.
|
|
*/
|
|
#[AsCommand(name: 'app:events:prune', description: 'Delete published domain events older than a retention window.')]
|
|
class PruneDomainEventsCommand extends Command
|
|
{
|
|
public function __construct(private readonly Connection $connection)
|
|
{
|
|
parent::__construct();
|
|
}
|
|
|
|
protected function configure(): void
|
|
{
|
|
$this
|
|
->addOption('days', null, InputOption::VALUE_REQUIRED, 'Retention window in days', '180')
|
|
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Report without deleting');
|
|
}
|
|
|
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
|
{
|
|
$io = new SymfonyStyle($input, $output);
|
|
$before = time() - max(1, (int) $input->getOption('days')) * 86400;
|
|
|
|
$count = (int) $this->connection->fetchOne(
|
|
'SELECT COUNT(*) FROM domain_events WHERE published_at IS NOT NULL AND occurred_at < ?',
|
|
[$before],
|
|
);
|
|
|
|
if ($count === 0) {
|
|
$io->success('رویداد قابل حذفی نیست.');
|
|
|
|
return Command::SUCCESS;
|
|
}
|
|
|
|
if ($input->getOption('dry-run')) {
|
|
$io->note(sprintf('%d رویداد حذف میشد.', $count));
|
|
|
|
return Command::SUCCESS;
|
|
}
|
|
|
|
$this->connection->executeStatement(
|
|
'DELETE FROM domain_events WHERE published_at IS NOT NULL AND occurred_at < ?',
|
|
[$before],
|
|
);
|
|
|
|
$io->success(sprintf('%d رویداد حذف شد.', $count));
|
|
|
|
return Command::SUCCESS;
|
|
}
|
|
}
|