"Allowed duration of a service combination" lived inside AppointmentController::serviceSlots(). Three upcoming callers need the same computation (PATCH duration validation, service-aware reschedule, reserve conversion); copying it would mean four variants with four different edge-case behaviours. The extraction is behaviour-preserving: BaseController::error() and ExceptionSubscriber emit an identical envelope, so returning $this->error() was replaced by throwing AppException with the same code/message/field. Tenant ownership now goes through TenantOwnershipChecker::belongsToPair() (the documented single point) instead of an inline section pair comparison. The repo property is named itemRepo on purpose: TenantLookupInventoryTest only counts recognised property names, so any other name would slip past the safety net. The naive duration sum is kept deliberately — switching to solo/additional minutes is task 04 and changes one line here. Task: docs/new_feture/taskes/task-00-service-mode-completion/ Slot-mode contract: unchanged (--group=slot-mode-frozen green) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
110 lines
5.3 KiB
PHP
110 lines
5.3 KiB
PHP
<?php
|
|
|
|
namespace App\Tests\Shared;
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
/**
|
|
* فهرستِ بازبینیشدهٔ جستوجوهای «uuid از درخواست → موجودیت محیطدار».
|
|
*
|
|
* سه نشتی این فاز همگی همین شکل را داشتند و هیچکدام در repository نبودند؛ در
|
|
* کنترلر و سرویس بودند، جایی که uuid از بدنه/کوئری میآید و کسی محیط را نمیسنجد.
|
|
* پس گارد هم باید همانجا باشد، نه روی DQLهای repository.
|
|
*
|
|
* قاعده: عدد هر فایل = تعداد findByUuid روی repositoryهای محیطدار در آن فایل، که
|
|
* همگی بازبینی شدهاند. اگر عدد عوض شود یعنی جستوجوی تازهای اضافه شده و باید
|
|
* ثابت شود محیطش بررسی میشود — با ownsRecord/ownsSection/ownedItem،
|
|
* TenantOwnershipChecker، یا لنگرزدن به ریشهای که خودش بررسی شده.
|
|
*
|
|
* @see \App\Shared\Tenant\TenantOwnershipChecker
|
|
* @see docs/architecture/tenancy.md
|
|
*/
|
|
class TenantLookupInventoryTest extends TestCase
|
|
{
|
|
/** repositoryهایی که موجودیت محیطدار یا فرزند aggregate برمیگردانند. */
|
|
private const TENANT_REPOSITORY_VARIABLES = [
|
|
'itemRepo', 'serviceItemRepo', 'sectionRepo', 'staffRepo',
|
|
'noteRepo', 'attachmentRepo', 'callRepo', 'messageRepo', 'medicalRepo',
|
|
'sessionRepo', 'sessionPaymentRepo', 'recordRepo',
|
|
'tenantInsuranceRepo', 'tenantTagRepo', 'packageRepo', 'ruleRepo',
|
|
];
|
|
|
|
/**
|
|
* فایل => تعداد جستوجوی بازبینیشده.
|
|
*
|
|
* @var array<string, int>
|
|
*/
|
|
private const REVIEWED = [
|
|
// assertServicesMatchContext (مسیر ثبت نوبت) + خودِ گارد. جستوجوی مسیرِ اسلاتِ
|
|
// سرویسی به ServiceBookingCalculator منتقل شد و همانجا شمرده میشود.
|
|
'src/Appointment/Controller/AppointmentController.php' => 2,
|
|
// TenantOwnershipChecker::belongsToPair() روی هر uuid، پیش از هر بررسی دیگر
|
|
'src/Appointment/Service/ServiceBookingCalculator.php' => 2,
|
|
// TenantOwnershipChecker روی بخش/سرویس/پرسنل و فهرست سرویسها
|
|
'src/Appointment/Controller/MyAppointmentsController.php' => 1,
|
|
// ownsSession / getEntityType روی صورتحساب، پرونده و مطالبه
|
|
'src/Billing/Controller/BillingController.php' => 3,
|
|
// ownsSection ×۹ و مقایسهٔ مستقیم جفت ×۳ (پکیج، کالا، پرسنل)
|
|
'src/ClinicService/Controller/ClinicServiceController.php' => 10,
|
|
'src/Discount/Controller/DiscountController.php' => 1,
|
|
// قرارداد بیمه با جفت، و سرویس با getSection()->getEntityType()
|
|
'src/Insurance/Controller/InsuranceController.php' => 5,
|
|
// ownedItem() / ownedPackage()
|
|
'src/Inventory/Controller/InventoryController.php' => 2,
|
|
'src/Inventory/Service/InventoryService.php' => 1,
|
|
// ownsRecord ×۳۳ و دو پرداختِ لنگرخورده به مراجعهٔ بررسیشده
|
|
'src/Patient/Controller/PatientController.php' => 35,
|
|
// TenantOwnershipChecker در هر سه حلقه (قیمتگذاری، ساخت، ویرایش) + کالا
|
|
'src/Patient/Service/PatientService.php' => 5,
|
|
// ownsStaff()
|
|
'src/Staff/Controller/StaffController.php' => 2,
|
|
];
|
|
|
|
private function projectDir(): string
|
|
{
|
|
return dirname(__DIR__, 2);
|
|
}
|
|
|
|
/** @return array<string, int> */
|
|
private function countLookups(): array
|
|
{
|
|
$pattern = '/->(' . implode('|', self::TENANT_REPOSITORY_VARIABLES) . ')->findByUuid\(/';
|
|
$counts = [];
|
|
|
|
$files = new \RecursiveIteratorIterator(
|
|
new \RecursiveDirectoryIterator($this->projectDir() . '/src', \FilesystemIterator::SKIP_DOTS),
|
|
);
|
|
|
|
foreach ($files as $file) {
|
|
if ($file->getExtension() !== 'php') {
|
|
continue;
|
|
}
|
|
|
|
$found = preg_match_all($pattern, (string) file_get_contents($file->getPathname()));
|
|
if ($found > 0) {
|
|
$relative = str_replace($this->projectDir() . '/', '', $file->getPathname());
|
|
$counts[$relative] = $found;
|
|
}
|
|
}
|
|
|
|
ksort($counts);
|
|
|
|
return $counts;
|
|
}
|
|
|
|
public function testNoUnreviewedTenantLookupExists(): void
|
|
{
|
|
$actual = $this->countLookups();
|
|
$expected = self::REVIEWED;
|
|
ksort($expected);
|
|
|
|
self::assertSame($expected, $actual, <<<'MSG'
|
|
جستوجوی «uuid از درخواست → موجودیت محیطدار» تغییر کرده.
|
|
|
|
برای هر مورد تازه ثابت کن محیطش بررسی میشود (ownsRecord/ownsSection/
|
|
ownedItem، TenantOwnershipChecker، یا لنگر به ریشهٔ بررسیشده) و بعد عدد
|
|
همان فایل را در REVIEWED بهروز کن. اگر عدد کم شده، فقط عدد را کم کن.
|
|
MSG);
|
|
}
|
|
}
|