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:
@@ -10,25 +10,14 @@ use App\Appointment\Plan\Entity\SegmentRequirement;
|
||||
use App\Appointment\Plan\Entity\SegmentTemplate;
|
||||
use App\Appointment\Plan\Service\AppointmentPlanBuilder;
|
||||
use App\Auth\Entity\User;
|
||||
use App\Cancellation\Entity\CancellationPolicy;
|
||||
use App\Cancellation\Entity\NoShowRecord;
|
||||
use App\ClinicService\Entity\CatalogCategory;
|
||||
use App\ClinicService\Entity\ItemGroup;
|
||||
use App\ClinicService\Entity\ItemGroupMember;
|
||||
use App\ClinicService\Entity\ServiceBranchOverride;
|
||||
use App\ClinicService\Entity\ServiceItem;
|
||||
use App\ClinicService\Entity\ServiceItemRelation;
|
||||
use App\Course\Entity\CourseProtocol;
|
||||
use App\Course\Entity\CourseProtocolStep;
|
||||
use App\Course\Entity\TreatmentCourse;
|
||||
use App\Doctor\Entity\DoctorAddress;
|
||||
use App\Package\Entity\Package;
|
||||
use App\Package\Entity\PackageService;
|
||||
use App\Package\Entity\PatientPackage;
|
||||
use App\Package\Entity\SessionCreditLedger;
|
||||
use App\Package\Service\CreditLedgerService;
|
||||
use App\Patient\Entity\PatientRecord;
|
||||
use App\Policy\Entity\Policy;
|
||||
use App\Pricing\Entity\PriceList;
|
||||
use App\Pricing\Entity\PriceListItem;
|
||||
use App\Pricing\Service\PriceSnapshotService;
|
||||
@@ -40,7 +29,6 @@ use App\Resource\Entity\ResourcePoolMember;
|
||||
use App\Resource\Entity\ResourceSkill;
|
||||
use App\Resource\Entity\ResourceType;
|
||||
use App\Resource\Entity\Skill;
|
||||
use App\Waitlist\Entity\WaitlistEntry;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
@@ -64,7 +52,6 @@ final class BookingEngineSeeder
|
||||
private readonly HoldService $holds,
|
||||
private readonly BookingService $booking,
|
||||
private readonly PriceSnapshotService $snapshots,
|
||||
private readonly CreditLedgerService $credits,
|
||||
) {}
|
||||
|
||||
private function em(): \Doctrine\ORM\EntityManagerInterface
|
||||
@@ -101,10 +88,7 @@ final class BookingEngineSeeder
|
||||
$counts['resources'] = $this->skillsPoolsAndExceptions($entityType, $entityId, $address, $devices, $deviceType);
|
||||
$counts['segments'] = $this->multiSegmentPlan($flagship, $deviceType, $entityType, $entityId, $address);
|
||||
$counts['pricing'] = $this->priceList($entityType, $entityId, $services);
|
||||
$counts['policies'] = $this->policies($entityType, $entityId, $services);
|
||||
$counts['booked'] = $this->realBookings($flagship, $address, $patients, $doctor, $clinic, $entityType, $entityId);
|
||||
$counts['packages'] = $this->packagesAndCourses($entityType, $entityId, $services, $patients);
|
||||
$counts['aftercare'] = $this->cancellationAndWaitlist($entityType, $entityId, $services, $patients, $doctor);
|
||||
|
||||
return $counts;
|
||||
}
|
||||
@@ -258,50 +242,6 @@ final class BookingEngineSeeder
|
||||
|
||||
// ── تسک ۰۹: سیاستها، یکی از هر دسته ────────────────────────────────────
|
||||
|
||||
private function policies(string $entityType, int $entityId, array $services): int
|
||||
{
|
||||
$service = $services[0];
|
||||
|
||||
$rows = [
|
||||
[Policy::CATEGORY_SELECTION, 'حداکثر سه ناحیه در یک نوبت',
|
||||
[['field' => 'item_count', 'operator' => 'greater_than', 'value' => 3]],
|
||||
[['type' => 'forbid']]],
|
||||
|
||||
[Policy::CATEGORY_ELIGIBILITY, 'زیر ۱۸ سال بدون رضایت والدین ممنوع',
|
||||
[['field' => 'patient_age', 'operator' => 'less_than', 'value' => 18]],
|
||||
[['type' => 'require_flag', 'value' => 'parental_consent']]],
|
||||
|
||||
[Policy::CATEGORY_RESOURCE, 'لیزر بدنِ کامل اپراتور ارشد میخواهد',
|
||||
[['field' => 'service_uuid', 'operator' => 'equals', 'value' => $service->getUuid()]],
|
||||
[['type' => 'require_resource', 'value' => 'laser']]],
|
||||
|
||||
[Policy::CATEGORY_TIMING, 'سبد بزرگ ۱۵ دقیقه وقت بیشتر میگیرد',
|
||||
[['field' => 'item_count', 'operator' => 'greater_or_equal', 'value' => 3]],
|
||||
[['type' => 'add_duration_minutes', 'value' => 15]]],
|
||||
|
||||
[Policy::CATEGORY_SPACING, 'حداقل ۲۸ روز فاصله بین دو جلسهٔ لیزر',
|
||||
[['field' => 'service_uuid', 'operator' => 'equals', 'value' => $service->getUuid()]],
|
||||
[['type' => 'min_days_between', 'value' => 28]]],
|
||||
|
||||
[Policy::CATEGORY_PRICING, 'تخفیف ۱۰٪ برای سبد بالای ۲۰ میلیون ریال',
|
||||
[['field' => 'subtotal_rials', 'operator' => 'greater_than', 'value' => 20_000_000]],
|
||||
[['type' => 'discount_percent', 'value' => 10]]],
|
||||
];
|
||||
|
||||
foreach ($rows as $i => [$category, $name, $conditions, $effects]) {
|
||||
$policy = new Policy($entityType, $entityId, $category, $name);
|
||||
$policy->setCondition(['match' => 'all', 'conditions' => $conditions])
|
||||
->setEffects($effects)
|
||||
->setPriority(10 * ($i + 1))
|
||||
->setActive(true);
|
||||
$this->em()->persist($policy);
|
||||
}
|
||||
|
||||
$this->em()->flush();
|
||||
|
||||
return count($rows);
|
||||
}
|
||||
|
||||
// ── تسک ۰۶ و ۰۷: رزرو واقعی روی تقویم منابع ─────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -374,130 +314,6 @@ final class BookingEngineSeeder
|
||||
|
||||
// ── تسک ۱۱ و ۱۲: پکیج، دفتر اعتبار، دورهٔ درمان ─────────────────────────
|
||||
|
||||
private function packagesAndCourses(string $entityType, int $entityId, array $services, array $patients): int
|
||||
{
|
||||
$service = $services[0];
|
||||
|
||||
$package = new Package($entityType, $entityId, 'پکیج ۶ جلسه لیزر', 6);
|
||||
$package->setPriceRials(60_000_000)->setValidityDays(365)->setActive(true);
|
||||
$this->em()->persist($package);
|
||||
$this->em()->persist(new PackageService($package, $service));
|
||||
$this->em()->flush();
|
||||
|
||||
$records = $this->em()->getRepository(PatientRecord::class)
|
||||
->findBy(['entityType' => $entityType, 'entityId' => $entityId], null, 3);
|
||||
|
||||
$made = 0;
|
||||
foreach ($records as $i => $record) {
|
||||
if ($i >= 2) {
|
||||
break;
|
||||
}
|
||||
|
||||
$patientPackage = new PatientPackage($package, $record);
|
||||
$this->em()->persist($patientPackage);
|
||||
$this->em()->flush();
|
||||
|
||||
// یک جلسه مصرفشده تا دفتر خالی نباشد؛ دفتر append-only است، پس مصرف هم
|
||||
// یک ردیف است نه کمکردن یک عدد.
|
||||
$this->credits->record($patientPackage, SessionCreditLedger::KIND_CONSUME, -1, null, $service, 'مصرف جلسهٔ اول');
|
||||
$made++;
|
||||
}
|
||||
|
||||
// دورهٔ درمان: پروتکل با پارامتر هر جلسه (انرژی لیزر بالا میرود) و یک دورهٔ فعال.
|
||||
$protocol = new CourseProtocol($service, 6, 21, 28, 45);
|
||||
$this->em()->persist($protocol);
|
||||
foreach (range(1, 6) as $n) {
|
||||
$this->em()->persist(new CourseProtocolStep($protocol, $n, ['energy' => 10 + $n * 2, 'spot_size' => 18]));
|
||||
}
|
||||
$this->em()->flush();
|
||||
|
||||
if ($records !== []) {
|
||||
$course = new TreatmentCourse($records[0], $protocol);
|
||||
$this->em()->persist($course);
|
||||
|
||||
// جلسههای دوره از روی گامهای پروتکل ساخته میشوند. بدون این ردیفها دوره
|
||||
// «۶ جلسهای» است ولی هیچ جلسهای ندارد، و پیشنهاد جلسهٔ بعد میگوید همهچیز
|
||||
// برنامهریزی شده — یعنی دقیقاً برعکس واقعیت.
|
||||
foreach ($protocol->getSteps() as $step) {
|
||||
$this->em()->persist(new \App\Course\Entity\CourseSession(
|
||||
$course,
|
||||
$step->getSessionNumber(),
|
||||
$step->getParams(),
|
||||
));
|
||||
}
|
||||
|
||||
$this->em()->flush();
|
||||
$made++;
|
||||
}
|
||||
|
||||
return $made;
|
||||
}
|
||||
|
||||
// ── تسک ۱۳: لغو، عدم حضور، لیست انتظار ──────────────────────────────────
|
||||
|
||||
private function cancellationAndWaitlist(string $entityType, int $entityId, array $services, array $patients, \App\Doctor\Entity\Doctor $doctor): int
|
||||
{
|
||||
// سیاست عمومیِ محیط + یک سیاست سختگیرانهتر روی سرویس گران — تا انتخابِ
|
||||
// «اختصاصیترین سیاست» چیزی برای انتخاب داشته باشد.
|
||||
$general = new CancellationPolicy($entityType, $entityId);
|
||||
$general->setFreeWindowHours(24)->setPenalty(CancellationPolicy::MODE_PERCENT, 30)
|
||||
->setDepositRefundable(true)->setCreditRefundable(true)->setNoShowThreshold(3)->setActive(true);
|
||||
$this->em()->persist($general);
|
||||
|
||||
$strict = new CancellationPolicy($entityType, $entityId, $services[0]);
|
||||
$strict->setFreeWindowHours(48)->setPenalty(CancellationPolicy::MODE_PERCENT, 50)
|
||||
->setDepositRefundable(false)->setCreditRefundable(false)->setNoShowThreshold(2)->setActive(true);
|
||||
$this->em()->persist($strict);
|
||||
$this->em()->flush();
|
||||
|
||||
$records = $this->em()->getRepository(PatientRecord::class)
|
||||
->findBy(['entityType' => $entityType, 'entityId' => $entityId], null, 4);
|
||||
|
||||
// عدم حضورِ ثبتشده روی نوبتهایی که وضعیتشان no_show است — بدون این ردیف،
|
||||
// شمارندهٔ عدم حضور بیمار همیشه صفر میماند و آستانهٔ سیاست هرگز فعال نمیشود.
|
||||
$noShows = $this->em()->getRepository(Appointment::class)
|
||||
->findBy(['doctor' => $doctor, 'status' => Appointment::STATUS_NO_SHOW], null, 2);
|
||||
|
||||
foreach ($noShows as $appointment) {
|
||||
$record = $this->recordFor($records, $appointment->getUser());
|
||||
if ($record !== null) {
|
||||
$this->em()->persist(new NoShowRecord($record, $appointment));
|
||||
}
|
||||
}
|
||||
|
||||
$made = 2 + count($noShows);
|
||||
|
||||
// لیست انتظار: دو نفر منتظرِ بازهٔ هفتهٔ آینده، یکی با ترجیح صبح.
|
||||
foreach (array_slice($records, 0, 2) as $i => $record) {
|
||||
$entry = new WaitlistEntry(
|
||||
$record,
|
||||
$services[min($i, count($services) - 1)],
|
||||
strtotime('+2 days 00:00'),
|
||||
strtotime('+9 days 00:00'),
|
||||
$this->em()->getRepository(\App\Doctor\Entity\DoctorAddress::class)->findOneBy([])?->getId(),
|
||||
);
|
||||
$entry->setPriority(10 - $i);
|
||||
if ($i === 0) {
|
||||
$entry->setPreferredDayParts(['morning']);
|
||||
}
|
||||
$this->em()->persist($entry);
|
||||
$made++;
|
||||
}
|
||||
|
||||
$this->em()->flush();
|
||||
|
||||
return $made;
|
||||
}
|
||||
|
||||
/** @param PatientRecord[] $records */
|
||||
private function recordFor(array $records, User $user): ?PatientRecord
|
||||
{
|
||||
foreach ($records as $record) {
|
||||
if ($record->getUser()->getId() === $user->getId()) {
|
||||
return $record;
|
||||
}
|
||||
}
|
||||
|
||||
return $records[0] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ class SeedScenariosCommand extends Command
|
||||
foreach ($this->engineCounts as $scenario => $counts) {
|
||||
$rows[] = array_merge(['سناریو ' . $scenario], array_values($counts));
|
||||
}
|
||||
$io->table(['سناریو', 'کاتالوگ', 'منابع', 'بخشها', 'قیمت', 'سیاست', 'رزرو واقعی', 'پکیج/دوره', 'لغو/انتظار'], $rows);
|
||||
$io->table(['سناریو', 'کاتالوگ', 'منابع', 'بخشها', 'قیمت', 'رزرو واقعی'], $rows);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
@@ -110,7 +110,6 @@ final class GlobalTables
|
||||
\App\ClinicService\Entity\Tariff::class => \App\ClinicService\Entity\ServiceItem::class,
|
||||
\App\ClinicService\Entity\ItemGroupMember::class => \App\ClinicService\Entity\ItemGroup::class,
|
||||
\App\Pricing\Entity\PriceListItem::class => \App\Pricing\Entity\PriceList::class,
|
||||
\App\Policy\Entity\PolicyVersionLog::class => \App\Policy\Entity\Policy::class,
|
||||
|
||||
\App\Billing\Entity\ClaimItem::class => \App\Billing\Entity\Claim::class,
|
||||
\App\Billing\Entity\ClaimStatusLog::class => \App\Billing\Entity\Claim::class,
|
||||
@@ -118,11 +117,7 @@ final class GlobalTables
|
||||
|
||||
\App\Inventory\Entity\InventoryPackageItem::class => \App\Inventory\Entity\InventoryPackage::class,
|
||||
|
||||
// سرویسهای یک پکیج جزئی از تعریف همان پکیجاند، نه دادهٔ مستقل.
|
||||
\App\Package\Entity\PackageService::class => \App\Package\Entity\Package::class,
|
||||
|
||||
// پارامترهای هر جلسه جزئی از تعریف همان پروتکلاند.
|
||||
\App\Course\Entity\CourseProtocolStep::class => \App\Course\Entity\CourseProtocol::class,
|
||||
|
||||
\App\Insurance\Entity\TenantInsuranceCategoryCoverage::class => \App\Insurance\Entity\TenantInsurance::class,
|
||||
\App\Insurance\Entity\TenantServiceCoverage::class => \App\Insurance\Entity\TenantInsurance::class,
|
||||
|
||||
Reference in New Issue
Block a user