Files
clinicpro/migrations/Version20260731061814.php
T
hamedandClaude Opus 5 584ea4067f feat(policy): six-category policy engine wired into the booking flow
Rules become data instead of code: a clinic can say "laser under 18 requires
parental consent" without a deploy.

Engine
- Policy / PolicyVersionLog entities, closed field/operator/effect lists per
  category (PolicySchema), condition validation at write time
- PolicyResolver: priority -> specificity -> age, combining effects by
  veto / max / sum / union
- A missing fact fails its clause instead of silently passing it
- Policies are drafts until activated, and are versioned rather than edited

Wiring
- selection -> ServiceSelectionValidator
- eligibility + spacing -> BookingPolicyGuard, at hold time not confirm time
- resource + timing -> AppointmentPlanBuilder, including template-less services
- pricing -> PricingEngine, alongside (not replacing) the manual discount

The condition column is named condition_json: `condition` is a MariaDB keyword
and broke every INSERT.

Tests: 17 in tests/Policy including NoPolicyRegressionTest, which pins that a
clinic with no policies sees byte-identical output to task 08.
Docs: docs/api/policy.md (real captured JSON) + docs/architecture/policy-engine.md.

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

45 lines
3.2 KiB
PHP

<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* The six-category policy engine of section 8: conditions drawn from a closed list,
* priority-ordered, versioned, and with no arbitrary code.
*
* Policies are versioned rather than edited in place. An appointment booked yesterday
* references the version that actually applied to it; rewriting a policy would make
* that reference point at text the appointment never saw.
*/
final class Version20260731061814 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add the six-category policy engine with version history';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE TABLE policies (id INT AUTO_INCREMENT NOT NULL, uuid VARCHAR(36) NOT NULL, category VARCHAR(20) NOT NULL, name VARCHAR(200) NOT NULL, condition_json JSON NOT NULL, effects JSON NOT NULL, priority SMALLINT DEFAULT 0 NOT NULL, valid_from INT DEFAULT NULL, valid_to INT DEFAULT NULL, version SMALLINT DEFAULT 1 NOT NULL, active TINYINT DEFAULT 0 NOT NULL, created_at INT NOT NULL, updated_at INT NOT NULL, entity_type VARCHAR(10) NOT NULL, entity_id INT NOT NULL, address_id INT DEFAULT NULL, service_item_id INT DEFAULT NULL, catalog_category_id INT DEFAULT NULL, UNIQUE INDEX UNIQ_E08BBFCED17F50A6 (uuid), INDEX IDX_E08BBFCEF5B7AF75 (address_id), INDEX IDX_E08BBFCEDDEB00C2 (service_item_id), INDEX IDX_E08BBFCE3F2BC4C (catalog_category_id), INDEX idx_policy_tenant_category (entity_type, entity_id, category, active), INDEX idx_policy_validity (valid_from, valid_to), PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4');
$this->addSql('CREATE TABLE policy_version_logs (id INT AUTO_INCREMENT NOT NULL, version SMALLINT NOT NULL, snapshot JSON NOT NULL, created_at INT NOT NULL, policy_id INT NOT NULL, INDEX IDX_B27F258B2D29E3C6 (policy_id), UNIQUE INDEX uniq_policy_version (policy_id, version), PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4');
$this->addSql('ALTER TABLE policies ADD CONSTRAINT FK_E08BBFCEF5B7AF75 FOREIGN KEY (address_id) REFERENCES doctor_addresses (id) ON DELETE CASCADE');
$this->addSql('ALTER TABLE policies ADD CONSTRAINT FK_E08BBFCEDDEB00C2 FOREIGN KEY (service_item_id) REFERENCES service_items (id) ON DELETE CASCADE');
$this->addSql('ALTER TABLE policies ADD CONSTRAINT FK_E08BBFCE3F2BC4C FOREIGN KEY (catalog_category_id) REFERENCES service_catalog_categories (id) ON DELETE CASCADE');
$this->addSql('ALTER TABLE policy_version_logs ADD CONSTRAINT FK_B27F258B2D29E3C6 FOREIGN KEY (policy_id) REFERENCES policies (id) ON DELETE CASCADE');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE policies DROP FOREIGN KEY FK_E08BBFCEF5B7AF75');
$this->addSql('ALTER TABLE policies DROP FOREIGN KEY FK_E08BBFCEDDEB00C2');
$this->addSql('ALTER TABLE policies DROP FOREIGN KEY FK_E08BBFCE3F2BC4C');
$this->addSql('ALTER TABLE policy_version_logs DROP FOREIGN KEY FK_B27F258B2D29E3C6');
$this->addSql('DROP TABLE policies');
$this->addSql('DROP TABLE policy_version_logs');
}
}