Files
clinicpro/migrations/Version20260731065427.php
hamedandClaude Opus 5 bcfa87bfad feat(policy): rule builder and mandatory dry-run sandbox
Task 09 shipped a powerful API that a non-technical clinic owner could not
safely use. This closes that gap: activation now requires having seen what the
rule actually does.

- PolicySimulator runs a policy against real past appointments and writes
  nothing: evaluation works on facts (never entities), the whole run sits in a
  transaction rolled back and cleared in `finally`, and a test counts rows in
  five sensitive tables before and after
- activate() now demands a simulation of the *same version* — a report for
  version 1 does not unlock version 2
- PolicyTemplateRegistry: six ready-made rules, so the common case never
  touches a raw condition
- Severity from the affected ratio; 0% is a warning too, since a rule that
  changes nothing usually has a condition that never matches
- An empty clinic still succeeds with a warning, otherwise a new clinic could
  never activate anything

Admin: PoliciesPage, PolicyFormPage, PolicySimulationPage, and a
PolicyConditionBuilder built entirely from GET /policy-schema — a test proves a
field that exists only in the schema shows up with no frontend change, and that
operators are filtered per field type.

The schema response now carries per-field metadata (label, type, meaningful
operators) so the form has one source of truth instead of two.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 10:44:28 +03:30

36 lines
1.9 KiB
PHP

<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260731065427 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE policy_simulation_runs (id INT AUTO_INCREMENT NOT NULL, uuid VARCHAR(36) NOT NULL, policy_version SMALLINT NOT NULL, sample_size SMALLINT NOT NULL, affected_count SMALLINT NOT NULL, severity VARCHAR(10) NOT NULL, report JSON NOT NULL, created_at INT NOT NULL, entity_type VARCHAR(10) NOT NULL, entity_id INT NOT NULL, policy_id INT NOT NULL, run_by INT DEFAULT NULL, UNIQUE INDEX UNIQ_F993F7C8D17F50A6 (uuid), INDEX IDX_F993F7C82D29E3C6 (policy_id), INDEX IDX_F993F7C84114BD6 (run_by), INDEX idx_psr_policy (policy_id, policy_version, created_at), INDEX idx_psr_tenant (entity_type, entity_id, created_at), PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4');
$this->addSql('ALTER TABLE policy_simulation_runs ADD CONSTRAINT FK_F993F7C82D29E3C6 FOREIGN KEY (policy_id) REFERENCES policies (id) ON DELETE CASCADE');
$this->addSql('ALTER TABLE policy_simulation_runs ADD CONSTRAINT FK_F993F7C84114BD6 FOREIGN KEY (run_by) REFERENCES users (id) ON DELETE SET NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE policy_simulation_runs DROP FOREIGN KEY FK_F993F7C82D29E3C6');
$this->addSql('ALTER TABLE policy_simulation_runs DROP FOREIGN KEY FK_F993F7C84114BD6');
$this->addSql('DROP TABLE policy_simulation_runs');
}
}