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>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Policy;
|
||||
|
||||
use App\Clinic\Entity\Clinic;
|
||||
use App\ClinicService\Entity\ServiceItem;
|
||||
use App\ClinicService\Entity\ServiceSection;
|
||||
use App\Doctor\Entity\DoctorAddress;
|
||||
use App\Tests\ApiTestCase;
|
||||
|
||||
/**
|
||||
* بدون هیچ قانونی، خروجیها باید **دقیقاً** همان تسک ۰۸ باشند.
|
||||
*
|
||||
* موتور قوانین یک لایهٔ افزودنی است نه بازنویسی: کلینیکی که هیچ قانونی ننوشته نباید
|
||||
* هیچ تفاوتی حس کند. این تست همان تضمین را میگیرد — و چون همهٔ قلابها در مسیر داغ
|
||||
* نشستهاند، شکستنش یعنی یک اثرِ پیشفرضِ ناخواسته وارد محاسبه شده.
|
||||
*/
|
||||
class NoPolicyRegressionTest extends ApiTestCase
|
||||
{
|
||||
public function testWithoutAnyPolicyThePlanAndQuoteAreUnchanged(): void
|
||||
{
|
||||
$user = $this->createUser(['ROLE_USER', 'ROLE_CLINIC']);
|
||||
$clinic = new Clinic($user);
|
||||
$clinic->setName('کلینیک بیقانون');
|
||||
$this->em->persist($clinic);
|
||||
$this->em->flush();
|
||||
|
||||
$section = new ServiceSection('clinic', $clinic->getId(), 'زیبایی');
|
||||
$this->em->persist($section);
|
||||
|
||||
$address = DoctorAddress::forClinic($clinic->getId());
|
||||
$address->setName('شعبهٔ مرکزی');
|
||||
$this->em->persist($address);
|
||||
$this->em->flush();
|
||||
|
||||
$service = new ServiceItem($section, 'لیزر');
|
||||
$service->setSoloDurationMinutes(20);
|
||||
$service->setPriceRials(1_000_000);
|
||||
$this->em->persist($service);
|
||||
$this->em->flush();
|
||||
|
||||
$plan = $this->authJson('POST', '/api/v1/appointment-plan/preview', $user, [
|
||||
'service_uuid' => $service->getUuid(),
|
||||
'branch_uuid' => $address->getUuid(),
|
||||
]);
|
||||
|
||||
self::assertSame(200, $this->responseCode(), json_encode($plan, JSON_UNESCAPED_UNICODE));
|
||||
self::assertSame(20, $plan['data']['total_minutes']);
|
||||
self::assertSame([0], array_column($plan['data']['segments'], 'offset_minutes'));
|
||||
|
||||
$quote = $this->authJson('POST', '/api/v1/pricing/quote', $user, [
|
||||
'service_uuid' => $service->getUuid(),
|
||||
'branch_uuid' => $address->getUuid(),
|
||||
]);
|
||||
|
||||
self::assertSame(1_000_000, $quote['data']['base_rials']);
|
||||
self::assertSame(0, $quote['data']['discount_rials']);
|
||||
self::assertSame(1_000_000, $quote['data']['final_rials']);
|
||||
|
||||
// نبودِ کلید مهمتر از صفر بودن مقدار است: کلیدِ خالی هم یعنی موتور چیزی
|
||||
// اعمال کرده که نباید میکرد.
|
||||
self::assertArrayNotHasKey('applied_policies', $quote['data']['breakdown']['sources']);
|
||||
|
||||
$selection = $this->authJson('POST', '/api/v1/service-selection/validate', $user, [
|
||||
'item_uuids' => [$service->getUuid()],
|
||||
'branch_uuid' => $address->getUuid(),
|
||||
]);
|
||||
|
||||
self::assertTrue($selection['data']['valid']);
|
||||
self::assertSame([], $selection['data']['errors']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user