Refactor booking system: Remove unused policies, packages, and related entities
- Removed package consumption flags and related properties from PriceQuote. - Eliminated unused domain event publishing for policies and waitlist in Schedule. - Cleaned up BookingEngineSeeder by removing package and policy related logic. - Updated SeedScenariosCommand to reflect removal of policies from output. - Dropped policy, package, treatment course, cancellation, waitlist, and domain event tables in migration. - Removed domain event assertions from tests related to resource blocking.
This commit is contained in:
@@ -16,10 +16,6 @@ use App\Resource\Entity\ResourceType;
|
||||
use App\Resource\Repository\ClinicResourceRepository;
|
||||
use App\Resource\Repository\ResourceTypeRepository;
|
||||
use App\Shared\Constant\ErrorCodes;
|
||||
use App\Policy\Entity\Policy;
|
||||
use App\Policy\Service\PolicySchema;
|
||||
use App\Policy\Engine\ResourcePolicyEngine;
|
||||
use App\Policy\Engine\TimingPolicyEngine;
|
||||
use App\Shared\Exception\AppException;
|
||||
|
||||
/**
|
||||
@@ -31,8 +27,6 @@ use App\Shared\Exception\AppException;
|
||||
final class AppointmentPlanBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TimingPolicyEngine $timingPolicies,
|
||||
private readonly ResourcePolicyEngine $resourcePolicies,
|
||||
private readonly SegmentTemplateRepository $templates,
|
||||
private readonly ClinicResourceRepository $resources,
|
||||
private readonly ResourceTypeRepository $types,
|
||||
@@ -111,14 +105,6 @@ final class AppointmentPlanBuilder
|
||||
array $segments,
|
||||
int $total,
|
||||
): AppointmentPlan {
|
||||
// ── قوانین دستهٔ «زمان» ────────────────────────────────────────────
|
||||
// اثرها روی **مجموع** نوبت اعمال میشوند نه روی یک بخش: «حداقل ۶۰ دقیقه»
|
||||
// یعنی کل جلسه، و کوتاه کردنِ یک بخش برای رسیدن به آن معنا ندارد.
|
||||
$total = $this->applyTimingPolicies($service, $selectedItems, $address, $segments, $total);
|
||||
|
||||
// ── قوانین دستهٔ «منبع» ─────────────────────────────────────────────
|
||||
$segments = $this->applyResourcePolicies($service, $selectedItems, $address, $segments);
|
||||
|
||||
if ($total > SegmentTemplate::MAX_TOTAL_MINUTES) {
|
||||
throw new AppException(
|
||||
ErrorCodes::ERR_VALIDATION_001,
|
||||
@@ -131,61 +117,6 @@ final class AppointmentPlanBuilder
|
||||
return new AppointmentPlan($segments, $total);
|
||||
}
|
||||
|
||||
/**
|
||||
* قوانین «زمان»: حداقل مدت (بیشترین برنده) و افزودن مدت (جمع).
|
||||
*
|
||||
* @param ServiceItem[] $selectedItems
|
||||
* @param list<PlannedSegment> $segments بهصورت ارجاع تغییر میکند
|
||||
*/
|
||||
private function applyTimingPolicies(
|
||||
ServiceItem $service,
|
||||
array $selectedItems,
|
||||
DoctorAddress $address,
|
||||
array &$segments,
|
||||
int $total,
|
||||
): int {
|
||||
$outcome = $this->timingPolicies->evaluate(
|
||||
$address->tenantEntityType(),
|
||||
$address->tenantEntityId(),
|
||||
[
|
||||
'service_uuid' => $service->getUuid(),
|
||||
'item_count' => count($selectedItems),
|
||||
'catalog_category' => $service->getCatalogCategory()?->getUuid(),
|
||||
],
|
||||
$address,
|
||||
$service,
|
||||
);
|
||||
|
||||
if ($outcome->effects === []) {
|
||||
return $total;
|
||||
}
|
||||
|
||||
$extra = (int) $outcome->effect(PolicySchema::EFFECT_ADD_DURATION, 0);
|
||||
$minimum = (int) $outcome->effect(PolicySchema::EFFECT_MIN_DURATION, 0);
|
||||
$target = max($total + $extra, $minimum);
|
||||
|
||||
if ($target === $total || $segments === []) {
|
||||
return $total;
|
||||
}
|
||||
|
||||
// مدت اضافه به **آخرین** بخش میرود: آفست بخشهای قبلی نباید عوض شود، وگرنه
|
||||
// برنامهای که کاربر تأیید کرده زیر پایش جابهجا میشود.
|
||||
$last = $segments[count($segments) - 1];
|
||||
$grown = $last->durationMinutes + ($target - $total);
|
||||
|
||||
$segments[count($segments) - 1] = new PlannedSegment(
|
||||
sequence: $last->sequence,
|
||||
name: $last->name,
|
||||
offsetMinutes: $last->offsetMinutes,
|
||||
durationMinutes: $grown,
|
||||
patientPresent: $last->patientPresent,
|
||||
mergeable: $last->mergeable,
|
||||
requirements: $last->requirements,
|
||||
);
|
||||
|
||||
return $target;
|
||||
}
|
||||
|
||||
/**
|
||||
* الگوهای سرویس اصلی **بهعلاوهٔ** الگوهای آیتمهای انتخابشده.
|
||||
*
|
||||
@@ -291,158 +222,10 @@ final class AppointmentPlanBuilder
|
||||
return $counts;
|
||||
}
|
||||
|
||||
/**
|
||||
* قوانین «منبع»: نقشی که قانون لازم میداند، اگر الگو نداشته باشد، اضافه میشود.
|
||||
*
|
||||
* نقشِ اضافهشده به **اولین بخشی که بیمار حاضر است** میچسبد، نه به همهٔ بخشها:
|
||||
* «سرپرست لازم است» یعنی سرپرست در جلسه حضور داشته باشد، نه اینکه تمام مدتِ
|
||||
* آمادهسازی هم اشغال شود.
|
||||
*
|
||||
* ممنوعیت هم اینجا خوانده میشود: قانونی که میگوید این ترکیب در این شعبه انجام
|
||||
* نمیشود، پیش از رسیدن به موتور دسترسپذیری جلوی کار را میگیرد.
|
||||
*
|
||||
* @param ServiceItem[] $selectedItems
|
||||
* @param list<PlannedSegment> $segments
|
||||
* @return list<PlannedSegment>
|
||||
*/
|
||||
private function applyResourcePolicies(
|
||||
ServiceItem $service,
|
||||
array $selectedItems,
|
||||
DoctorAddress $address,
|
||||
array $segments,
|
||||
): array {
|
||||
if ($segments === []) {
|
||||
return $segments;
|
||||
}
|
||||
|
||||
$outcome = $this->resourcePolicies->evaluate(
|
||||
$address->tenantEntityType(),
|
||||
$address->tenantEntityId(),
|
||||
[
|
||||
'service_uuid' => $service->getUuid(),
|
||||
'catalog_category' => $service->getCatalogCategory()?->getUuid(),
|
||||
'item_count' => count($selectedItems),
|
||||
],
|
||||
$address,
|
||||
$service,
|
||||
);
|
||||
|
||||
if ($outcome->isForbidden()) {
|
||||
throw new AppException(
|
||||
ErrorCodes::ERR_VALIDATION_001,
|
||||
implode(' ', $outcome->forbidReasons),
|
||||
422,
|
||||
'service_uuid',
|
||||
);
|
||||
}
|
||||
|
||||
$required = (array) $outcome->effect(PolicySchema::EFFECT_REQUIRE_RESOURCE, []);
|
||||
|
||||
if ($required === []) {
|
||||
return $segments;
|
||||
}
|
||||
|
||||
$present = [];
|
||||
foreach ($segments as $segment) {
|
||||
foreach ($segment->requirements as $requirement) {
|
||||
$present[$requirement->role] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$targetIndex = $this->firstPatientPresentIndex($segments);
|
||||
$extra = [];
|
||||
|
||||
foreach ($required as $code) {
|
||||
if (!is_string($code) || isset($present[$code])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$extra[] = $this->requirementForRole($code, $address, $outcome->appliedPolicies);
|
||||
}
|
||||
|
||||
if ($extra === []) {
|
||||
return $segments;
|
||||
}
|
||||
|
||||
$target = $segments[$targetIndex];
|
||||
|
||||
$segments[$targetIndex] = new PlannedSegment(
|
||||
sequence: $target->sequence,
|
||||
name: $target->name,
|
||||
offsetMinutes: $target->offsetMinutes,
|
||||
durationMinutes: $target->durationMinutes,
|
||||
patientPresent: $target->patientPresent,
|
||||
mergeable: $target->mergeable,
|
||||
requirements: [...$target->requirements, ...$extra],
|
||||
);
|
||||
|
||||
return array_values($segments);
|
||||
}
|
||||
|
||||
/** @param list<PlannedSegment> $segments */
|
||||
private function firstPatientPresentIndex(array $segments): int
|
||||
{
|
||||
foreach ($segments as $index => $segment) {
|
||||
if ($segment->patientPresent) {
|
||||
return $index;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* قانونی که نقشِ ناشناخته یا بیمنبع میخواهد **خطاست، نه بیاثر**: در سکوت رد
|
||||
* کردنش یعنی کلینیک فکر کند قانونش اجرا میشود در حالی که هیچوقت نشده.
|
||||
*/
|
||||
/**
|
||||
* @param list<array<string, mixed>> $appliedPolicies برای اینکه پیام بگوید **کدام** قانون
|
||||
*/
|
||||
private function requirementForRole(string $code, DoctorAddress $address, array $appliedPolicies = []): PlannedRequirement
|
||||
{
|
||||
// بدون نام قانون، اپراتور میداند چه چیزی کم است ولی نه چرا لازم شده — و بین ده
|
||||
// قانون فعال باید حدس بزند کدام را خاموش کند.
|
||||
$names = array_values(array_filter(array_map(
|
||||
static fn (array $p): ?string => is_string($p['name'] ?? null) ? $p['name'] : null,
|
||||
$appliedPolicies,
|
||||
)));
|
||||
$because = $names === [] ? '' : sprintf(' (قانون: %s)', implode('، ', $names));
|
||||
|
||||
$type = $this->types->findByCode($address->tenantEntityType(), $address->tenantEntityId(), $code);
|
||||
|
||||
if ($type === null) {
|
||||
throw new AppException(
|
||||
ErrorCodes::ERR_VALIDATION_001,
|
||||
sprintf('قانون منبعی نقش «%s» را لازم دارد که در این محیط تعریف نشده است%s', $code, $because),
|
||||
422,
|
||||
'requirements',
|
||||
);
|
||||
}
|
||||
|
||||
$eligible = array_values($this->resources->findEligible($address, $type, []));
|
||||
|
||||
if ($eligible === []) {
|
||||
throw new AppException(
|
||||
ErrorCodes::ERR_NO_ELIGIBLE_RESOURCE,
|
||||
sprintf('هیچ %s در شعبهٔ «%s» موجود نیست%s', $type->getName(), $address->getName() ?? '—', $because),
|
||||
422,
|
||||
'requirements',
|
||||
);
|
||||
}
|
||||
|
||||
return new PlannedRequirement(
|
||||
role: $type->getCode(),
|
||||
roleName: $type->getName(),
|
||||
count: 1,
|
||||
occupancy: SegmentRequirement::OCCUPANCY_EXCLUSIVE,
|
||||
constraints: [],
|
||||
eligible: $eligible,
|
||||
skillName: null,
|
||||
setupMinutes: $this->maxOf($eligible, static fn (ClinicResource $r): int => $r->getSetupMinutes()),
|
||||
cleanupMinutes: $this->maxOf($eligible, static fn (ClinicResource $r): int => $r->getCleanupMinutes()),
|
||||
);
|
||||
}
|
||||
|
||||
/** @return list<PlannedRequirement> */
|
||||
private function planRequirements(
|
||||
SegmentTemplate $template,
|
||||
|
||||
Reference in New Issue
Block a user