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>
This commit is contained in:
@@ -63,6 +63,10 @@ class PolicyEngineTest extends ApiTestCase
|
||||
return $created['data'];
|
||||
}
|
||||
|
||||
// فعالسازی از تسک ۱۰ به بعد یک اجرای آزمایشی از **همین نسخه** میخواهد.
|
||||
$this->authJson('POST', "/api/v1/policy/{$created['data']['uuid']}/simulate", $user);
|
||||
self::assertSame(201, $this->responseCode());
|
||||
|
||||
$active = $this->authJson('POST', "/api/v1/policy/{$created['data']['uuid']}/activate", $user);
|
||||
self::assertSame(200, $this->responseCode(), json_encode($active, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
@@ -128,6 +132,15 @@ class PolicyEngineTest extends ApiTestCase
|
||||
|
||||
// فیلد قیمتی در دستهٔ زمان جایی ندارد — همین بستهبودن نکتهٔ اصلی شِماست.
|
||||
self::assertNotContains('subtotal_rials', $schema['timing']['fields']);
|
||||
|
||||
// فرم باید عملگرها را per فیلد فیلتر کند، وگرنه کاربر «برچسب > ۵» میسازد و
|
||||
// ۴۲۲ میگیرد بیآنکه بفهمد چرا.
|
||||
$meta = array_column($schema['eligibility']['field_meta'], null, 'key');
|
||||
|
||||
self::assertSame('int', $meta['patient_age']['type']);
|
||||
self::assertSame(['equals', 'not_equals', 'greater_than', 'less_than'], $meta['patient_age']['operators']);
|
||||
self::assertSame(['contains'], $meta['patient_tags']['operators']);
|
||||
self::assertSame('سن بیمار', $meta['patient_age']['label']);
|
||||
}
|
||||
|
||||
public function testFieldOutsideTheCategoryIsRejectedAtCreateTime(): void
|
||||
@@ -390,6 +403,8 @@ class PolicyEngineTest extends ApiTestCase
|
||||
self::assertSame(200, $this->responseCode(), json_encode($updated, JSON_UNESCAPED_UNICODE));
|
||||
self::assertSame(2, $updated['data']['version']);
|
||||
|
||||
// نسخهٔ تازه فعال میماند؛ آزمایش دوباره لازم نیست چون قانون از قبل فعال بود.
|
||||
|
||||
$second = $this->quote($user, $service, $address);
|
||||
self::assertSame(200_000, $second['data']['discount_rials']);
|
||||
self::assertSame(2, $second['data']['breakdown']['sources']['applied_policies'][0]['version']);
|
||||
|
||||
Reference in New Issue
Block a user