更新bug
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
use app\adminapi\logic\tcm\DiagnosisLogic;
|
||||
|
||||
function callRecordIdentityExpect(bool $condition, string $message): void
|
||||
{
|
||||
if (!$condition) {
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
}
|
||||
|
||||
function callRecordIdentityMethodSource(ReflectionMethod $method): string
|
||||
{
|
||||
$lines = file($method->getFileName());
|
||||
if (!is_array($lines)) {
|
||||
throw new RuntimeException('call-record lifecycle source is readable');
|
||||
}
|
||||
|
||||
return implode('', array_slice(
|
||||
$lines,
|
||||
$method->getStartLine() - 1,
|
||||
$method->getEndLine() - $method->getStartLine() + 1
|
||||
));
|
||||
}
|
||||
|
||||
$logic = new ReflectionClass(DiagnosisLogic::class);
|
||||
$endCall = callRecordIdentityMethodSource($logic->getMethod('endCall'));
|
||||
$bindCallRoom = callRecordIdentityMethodSource($logic->getMethod('bindCallRoom'));
|
||||
$startCloudRecording = callRecordIdentityMethodSource($logic->getMethod('startCloudRecording'));
|
||||
|
||||
foreach (
|
||||
[
|
||||
'endCall' => $endCall,
|
||||
'bindCallRoom' => $bindCallRoom,
|
||||
'startCloudRecording' => $startCloudRecording,
|
||||
] as $methodName => $source
|
||||
) {
|
||||
callRecordIdentityExpect(
|
||||
str_contains($source, "\$callRecordId = (int)(\$params['call_record_id'] ?? 0)")
|
||||
&& str_contains($source, 'if ($callRecordId > 0)')
|
||||
&& str_contains($source, "CallRecord::where('id', \$callRecordId)")
|
||||
&& str_contains($source, "->where('diagnosis_id', \$diagnosisId)")
|
||||
&& str_contains($source, "->where('caller_id', \$adminId)")
|
||||
&& str_contains($source, "->where('caller_type', 'doctor')"),
|
||||
$methodName . ' uses the exact doctor-owned call record when its id is supplied'
|
||||
);
|
||||
}
|
||||
|
||||
callRecordIdentityExpect(
|
||||
str_contains($bindCallRoom, "'call_record_id' => (int)\$record['id']")
|
||||
&& str_contains($bindCallRoom, 'startCloudRecording(['),
|
||||
'room binding forwards the exact saved record to cloud recording'
|
||||
);
|
||||
callRecordIdentityExpect(
|
||||
str_contains($endCall, "(int)(\$record['status'] ?? 0) === 2")
|
||||
&& str_contains($endCall, 'return true;'),
|
||||
'ending the same exact call remains idempotent'
|
||||
);
|
||||
|
||||
$workspace = dirname(__DIR__, 2);
|
||||
$repository = (string) file_get_contents(
|
||||
$workspace . '/app/src/doctor_workstation/services/repository.py'
|
||||
);
|
||||
$lifecycle = (string) file_get_contents(
|
||||
$workspace . '/app/src/doctor_workstation/video/lifecycle.py'
|
||||
);
|
||||
callRecordIdentityExpect(
|
||||
substr_count($repository, 'body["call_record_id"] = normalized_id') >= 2,
|
||||
'desktop repository sends call_record_id for bind and end requests'
|
||||
);
|
||||
callRecordIdentityExpect(
|
||||
substr_count($lifecycle, '"call_record_id": record_id') >= 2,
|
||||
'desktop lifecycle keeps the created call record identity through later phases'
|
||||
);
|
||||
|
||||
echo "Call-record lifecycle identity contract: OK\n";
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
use app\adminapi\controller\tcm\DiagnosisController;
|
||||
use app\adminapi\logic\tcm\DiagnosisLogic;
|
||||
use app\adminapi\validate\tcm\DiagnosisValidate;
|
||||
|
||||
function callSignatureExpect(bool $condition, string $message): void
|
||||
{
|
||||
if (!$condition) {
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
}
|
||||
|
||||
function callSignatureMethodSource(ReflectionMethod $method): string
|
||||
{
|
||||
$lines = file($method->getFileName());
|
||||
if (!is_array($lines)) {
|
||||
throw new RuntimeException('call signature source is readable');
|
||||
}
|
||||
|
||||
return implode('', array_slice(
|
||||
$lines,
|
||||
$method->getStartLine() - 1,
|
||||
$method->getEndLine() - $method->getStartLine() + 1
|
||||
));
|
||||
}
|
||||
|
||||
$logicMethod = (new ReflectionClass(DiagnosisLogic::class))->getMethod('getCallSignature');
|
||||
$logicSource = callSignatureMethodSource($logicMethod);
|
||||
$controllerSource = callSignatureMethodSource(
|
||||
(new ReflectionClass(DiagnosisController::class))->getMethod('getCallSignature')
|
||||
);
|
||||
$validatorSource = callSignatureMethodSource(
|
||||
(new ReflectionClass(DiagnosisValidate::class))->getMethod('sceneCallIdentity')
|
||||
);
|
||||
|
||||
callSignatureExpect(
|
||||
$logicMethod->getNumberOfParameters() === 2,
|
||||
'call signature accepts request params and authenticated admin context'
|
||||
);
|
||||
callSignatureExpect(
|
||||
str_contains($logicSource, 'canManageDiagnosis($diagnosisId, $adminId, $adminInfo)')
|
||||
&& strpos($logicSource, 'canManageDiagnosis($diagnosisId, $adminId, $adminInfo)')
|
||||
< strpos($logicSource, "Diagnosis::where('id', \$diagnosisId)")
|
||||
&& strpos($logicSource, "Diagnosis::where('id', \$diagnosisId)")
|
||||
< strpos($logicSource, 'self::getTrtcConfig()'),
|
||||
'row authorization and the exact diagnosis/patient pair are checked before TRTC work'
|
||||
);
|
||||
callSignatureExpect(
|
||||
str_contains($logicSource, "->where('patient_id', \$patientId)")
|
||||
&& str_contains($logicSource, "'diagnosis_id' => \$diagnosisId")
|
||||
&& str_contains($logicSource, "'patient_id' => \$patientId")
|
||||
&& !str_contains($logicSource, "Diagnosis::where('id', \$patientId)"),
|
||||
'diagnosis id and source patient id stay distinct throughout the signature contract'
|
||||
);
|
||||
callSignatureExpect(
|
||||
str_contains($controllerSource, "goCheck('callIdentity')")
|
||||
&& str_contains(
|
||||
$controllerSource,
|
||||
'DiagnosisLogic::getCallSignature($params, $this->adminInfo)'
|
||||
),
|
||||
'controller validates both ids and forwards the authenticated row-scope context'
|
||||
);
|
||||
callSignatureExpect(
|
||||
str_contains($validatorSource, "only(['diagnosis_id', 'patient_id'])")
|
||||
&& str_contains($validatorSource, "append('patient_id', 'require|integer|gt:0')"),
|
||||
'call identity validation requires positive diagnosis and patient ids'
|
||||
);
|
||||
|
||||
$root = dirname(__DIR__, 2);
|
||||
foreach (
|
||||
[
|
||||
$root . '/admin/src/views/tcm/appointment/list.vue',
|
||||
$root . '/admin/src/views/tcm/appointment/list_h5.vue',
|
||||
$root . '/admin/src/views/patient/reception/index.vue',
|
||||
] as $callerPath
|
||||
) {
|
||||
$caller = (string) file_get_contents($callerPath);
|
||||
callSignatureExpect(
|
||||
str_contains($caller, 'patient_id: sourcePatientId')
|
||||
&& str_contains($caller, 'patientId: sourcePatientId')
|
||||
&& str_contains($caller, 'diagnosis_id: diagnosisId'),
|
||||
basename($callerPath) . ' keeps appointment, diagnosis and source patient ids separate'
|
||||
);
|
||||
}
|
||||
|
||||
echo "Call signature identity contract: OK\n";
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use app\adminapi\logic\stats\ConversionLogic;
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
function conversionFanRuleExpect(bool $condition, string $message): void
|
||||
{
|
||||
if (!$condition) {
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
}
|
||||
|
||||
$method = new ReflectionMethod(ConversionLogic::class, 'loadFanRows');
|
||||
$sourceLines = file($method->getFileName());
|
||||
if ($sourceLines === false) {
|
||||
throw new RuntimeException('无法读取加粉统计源码');
|
||||
}
|
||||
$methodSource = implode('', array_slice(
|
||||
$sourceLines,
|
||||
$method->getStartLine() - 1,
|
||||
$method->getEndLine() - $method->getStartLine() + 1
|
||||
));
|
||||
|
||||
conversionFanRuleExpect(
|
||||
str_contains($methodSource, "->where('e.change_type', 'add_external_contact')"),
|
||||
'加粉必须继续以企微 add_external_contact 事件为事实来源'
|
||||
);
|
||||
conversionFanRuleExpect(
|
||||
!str_contains($methodSource, 'msg_audit_approved'),
|
||||
'会话存档同意是独立能力,不能再次成为加粉统计的硬性条件'
|
||||
);
|
||||
conversionFanRuleExpect(
|
||||
str_contains($methodSource, "['del_external_contact', \$endTimestamp]"),
|
||||
'加粉统计必须继续排除区间内已删除客户'
|
||||
);
|
||||
conversionFanRuleExpect(
|
||||
str_contains($methodSource, "['add_external_contact', \$startTimestamp]"),
|
||||
'加粉统计必须继续排除区间开始前已添加的重加客户'
|
||||
);
|
||||
conversionFanRuleExpect(
|
||||
str_contains($methodSource, "->group('e.user_id, e.external_userid')"),
|
||||
'加粉统计必须继续按员工和客户去重'
|
||||
);
|
||||
|
||||
$excludeMethod = new ReflectionMethod(ConversionLogic::class, 'excludeUncountedFanPairs');
|
||||
$excludeSource = implode('', array_slice(
|
||||
$sourceLines,
|
||||
$excludeMethod->getStartLine() - 1,
|
||||
$excludeMethod->getEndLine() - $excludeMethod->getStartLine() + 1
|
||||
));
|
||||
conversionFanRuleExpect(
|
||||
str_contains($excludeSource, '[1, 2, 3, 201, 202]'),
|
||||
'取消会话存档条件后仍须排除扫一扫、搜手机号、名片分享及继承/分配客户'
|
||||
);
|
||||
|
||||
$pageSource = file_get_contents(
|
||||
dirname(__DIR__, 2) . '/admin/src/views/first_visit/conversion/index.vue'
|
||||
);
|
||||
conversionFanRuleExpect(
|
||||
is_string($pageSource) && !str_contains($pageSource, '须会话同意'),
|
||||
'页面口径说明不能继续宣称加粉依赖会话存档同意'
|
||||
);
|
||||
|
||||
echo "Conversion fan event rule: OK\n";
|
||||
@@ -121,6 +121,9 @@ $myPatientScopeMethod = diagnosisWorkspaceMethodSource(
|
||||
$diagnosisReadonlyAuthMethod = diagnosisWorkspaceMethodSource(
|
||||
(new ReflectionClass(DiagnosisLogic::class))->getMethod('canViewReadonlyDiagnosis')
|
||||
);
|
||||
$diagnosisManageAuthMethod = diagnosisWorkspaceMethodSource(
|
||||
(new ReflectionClass(DiagnosisLogic::class))->getMethod('canManageDiagnosis')
|
||||
);
|
||||
$diagnosisAiAuthMethod = diagnosisWorkspaceMethodSource(
|
||||
(new ReflectionClass(DiagnosisAiLogic::class))->getMethod('loadAuthorizedDiagnosis')
|
||||
);
|
||||
@@ -161,9 +164,16 @@ diagnosisWorkspaceAuthExpect(
|
||||
'DataScope ALL is reserved for team roles before ordinary doctor and assistant self-relations'
|
||||
);
|
||||
diagnosisWorkspaceAuthExpect(
|
||||
str_contains($diagnosisReadonlyAuthMethod, 'MyPatientLogic::canAccessDiagnosis(')
|
||||
&& !str_contains($diagnosisReadonlyAuthMethod, 'DataScopeService::getVisibleAdminIds'),
|
||||
'readonly diagnosis authorization reuses the canonical patient row policy'
|
||||
str_contains($diagnosisReadonlyAuthMethod, "Diagnosis::where('id', \$diagnosisId)")
|
||||
&& str_contains($diagnosisReadonlyAuthMethod, "in_array(2, \$roleIds, true)")
|
||||
&& str_contains($diagnosisReadonlyAuthMethod, "\$query->where('assistant_id', \$adminId)")
|
||||
&& str_contains($diagnosisReadonlyAuthMethod, 'DataScopeService::getVisibleAdminIds(')
|
||||
&& !str_contains($diagnosisReadonlyAuthMethod, 'MyPatientLogic::canAccessDiagnosis('),
|
||||
'readonly diagnosis authorization matches diagnosis-list visibility instead of my-patient ownership'
|
||||
);
|
||||
diagnosisWorkspaceAuthExpect(
|
||||
str_contains($diagnosisManageAuthMethod, 'MyPatientLogic::canAccessDiagnosis('),
|
||||
'diagnosis write authorization keeps the canonical my-patient ownership policy'
|
||||
);
|
||||
diagnosisWorkspaceAuthExpect(
|
||||
str_contains($diagnosisAiAuthMethod, 'MyPatientLogic::canAccessDiagnosis(')
|
||||
@@ -192,10 +202,10 @@ diagnosisWorkspaceAuthExpect(
|
||||
'doctorNotes authorizes the diagnosis before reading notes'
|
||||
);
|
||||
diagnosisWorkspaceAuthExpect(
|
||||
str_contains($addDoctorNoteControllerMethod, 'DiagnosisLogic::canViewReadonlyDiagnosis(')
|
||||
&& strpos($addDoctorNoteControllerMethod, 'DiagnosisLogic::canViewReadonlyDiagnosis(')
|
||||
str_contains($addDoctorNoteControllerMethod, 'DiagnosisLogic::canManageDiagnosis(')
|
||||
&& strpos($addDoctorNoteControllerMethod, 'DiagnosisLogic::canManageDiagnosis(')
|
||||
< strpos($addDoctorNoteControllerMethod, 'DoctorNoteLogic::addOrAppend('),
|
||||
'addDoctorNote authorizes the diagnosis before writing any note data'
|
||||
'addDoctorNote keeps the stricter ownership check before writing any note data'
|
||||
);
|
||||
diagnosisWorkspaceAuthExpect(
|
||||
str_contains($receptionMethod, 'appointmentRowManageableByAdmin(')
|
||||
@@ -204,10 +214,10 @@ diagnosisWorkspaceAuthExpect(
|
||||
'reception authorizes the appointment before loading its detail DTO'
|
||||
);
|
||||
diagnosisWorkspaceAuthExpect(
|
||||
str_contains($prescriptionListMethod, 'MyPatientLogic::canAccessDiagnosis(')
|
||||
&& strpos($prescriptionListMethod, 'MyPatientLogic::canAccessDiagnosis(')
|
||||
str_contains($prescriptionListMethod, 'DiagnosisLogic::canViewReadonlyDiagnosis(')
|
||||
&& strpos($prescriptionListMethod, 'DiagnosisLogic::canViewReadonlyDiagnosis(')
|
||||
< strpos($prescriptionListMethod, "Prescription::where('diagnosis_id', \$diagnosisId)"),
|
||||
'listByDiagnosis authorizes its parent diagnosis before the first prescription SQL query'
|
||||
'listByDiagnosis uses read visibility before the first prescription SQL query'
|
||||
);
|
||||
diagnosisWorkspaceAuthExpect(
|
||||
str_contains($prescriptionLogicSource, 'self::canViewPrescription($row, $viewerAdminId, $viewerAdminInfo)')
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
final class LocalAudioUploadContractExpect
|
||||
{
|
||||
public static function contains(string $haystack, string $needle, string $message): void
|
||||
{
|
||||
if (!str_contains($haystack, $needle)) {
|
||||
throw new RuntimeException($message . ': missing ' . $needle);
|
||||
}
|
||||
}
|
||||
|
||||
public static function isTrue(bool $condition, string $message): void
|
||||
{
|
||||
if (!$condition) {
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$root = dirname(__DIR__);
|
||||
$controllerPath = $root . '/app/adminapi/controller/tcm/DiagnosisController.php';
|
||||
$logicPath = $root . '/app/adminapi/logic/tcm/DiagnosisLogic.php';
|
||||
$migrationPath = $root . '/database/migrations/2026_08_19_add_local_call_audio_fields.sql';
|
||||
|
||||
$controller = (string)file_get_contents($controllerPath);
|
||||
$logic = (string)file_get_contents($logicPath);
|
||||
$migration = (string)file_get_contents($migrationPath);
|
||||
|
||||
LocalAudioUploadContractExpect::contains(
|
||||
$controller,
|
||||
'public function uploadCallRecording()',
|
||||
'The ThinkPHP controller must expose the desktop upload action'
|
||||
);
|
||||
LocalAudioUploadContractExpect::contains(
|
||||
$controller,
|
||||
'DiagnosisLogic::uploadCallRecording($params)',
|
||||
'The controller must delegate multipart uploads to the diagnosis logic'
|
||||
);
|
||||
|
||||
foreach (
|
||||
[
|
||||
"'webm'",
|
||||
"'ogg'",
|
||||
"'media_kind' => \$isLocalAudio ? 'local_audio' : 'video'",
|
||||
'attachLocalCallAudio($attachmentParams)',
|
||||
"'local_audio_urls'",
|
||||
"'local_audio_status' => 2",
|
||||
"'local_audio_urls_list'",
|
||||
"'local_audio_status_text'",
|
||||
"'uploads/audio/'",
|
||||
'FileEnum::FILE_TYPE',
|
||||
] as $needle
|
||||
) {
|
||||
LocalAudioUploadContractExpect::contains(
|
||||
$logic,
|
||||
$needle,
|
||||
'Local audio upload and DTO contract must remain complete'
|
||||
);
|
||||
}
|
||||
|
||||
foreach (
|
||||
[
|
||||
"'uploads/video/'",
|
||||
'FileEnum::VIDEO_TYPE',
|
||||
'attachLocalCallRecording($attachmentParams)',
|
||||
"'recording_status' => \$isLocalAudio ? 0 : 2",
|
||||
] as $needle
|
||||
) {
|
||||
LocalAudioUploadContractExpect::contains(
|
||||
$logic,
|
||||
$needle,
|
||||
'Manual video uploads must remain separate from local audio'
|
||||
);
|
||||
}
|
||||
|
||||
LocalAudioUploadContractExpect::isTrue(
|
||||
str_contains($logic, '!$isAmbiguousWebm && $hasAudioMime'),
|
||||
'Known non-WebM video extensions must not be misclassified when an old client sends audio/webm'
|
||||
);
|
||||
|
||||
foreach (['zyt_tcm_call_record', 'local_audio_urls', 'local_audio_status'] as $needle) {
|
||||
LocalAudioUploadContractExpect::contains(
|
||||
$migration,
|
||||
$needle,
|
||||
'The database migration must include every local audio field'
|
||||
);
|
||||
}
|
||||
|
||||
// Exercise the real internal-file path used after all recording chunks are
|
||||
// merged. A source-code assertion alone did not catch the missing realPath key
|
||||
// (or the missing File object required by the local storage engine).
|
||||
$sourceBase = tempnam(sys_get_temp_dir(), 'zyt-audio-source-');
|
||||
LocalAudioUploadContractExpect::isTrue(
|
||||
is_string($sourceBase),
|
||||
'A temporary local-audio source file must be creatable'
|
||||
);
|
||||
$sourcePath = $sourceBase . '.webm';
|
||||
LocalAudioUploadContractExpect::isTrue(
|
||||
rename($sourceBase, $sourcePath),
|
||||
'The temporary local-audio source must keep its WebM extension'
|
||||
);
|
||||
$payload = "\x1A\x45\xDF\xA3local-audio-regression";
|
||||
LocalAudioUploadContractExpect::isTrue(
|
||||
file_put_contents($sourcePath, $payload) === strlen($payload),
|
||||
'The local-audio fixture must be written completely'
|
||||
);
|
||||
$targetDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR
|
||||
. 'zyt-local-audio-target-' . bin2hex(random_bytes(6));
|
||||
|
||||
try {
|
||||
$localStorage = new \app\common\service\storage\engine\Local();
|
||||
$localStorage->setUploadFileByReal($sourcePath);
|
||||
$fileInfo = $localStorage->getFileInfo();
|
||||
|
||||
LocalAudioUploadContractExpect::isTrue(
|
||||
isset($fileInfo['realPath']) && $fileInfo['realPath'] === realpath($sourcePath),
|
||||
'Internal storage metadata must include the merged recording realPath'
|
||||
);
|
||||
LocalAudioUploadContractExpect::isTrue(
|
||||
($fileInfo['tmp_name'] ?? '') === realpath($sourcePath),
|
||||
'Internal storage metadata must preserve a compatible temporary path'
|
||||
);
|
||||
LocalAudioUploadContractExpect::isTrue(
|
||||
str_ends_with($localStorage->getFileName(), '.webm'),
|
||||
'The generated storage name must preserve the recording extension'
|
||||
);
|
||||
LocalAudioUploadContractExpect::isTrue(
|
||||
$localStorage->upload($targetDir),
|
||||
'The local storage driver must accept a merged internal recording'
|
||||
);
|
||||
|
||||
$storedPath = $targetDir . DIRECTORY_SEPARATOR . $localStorage->getFileName();
|
||||
LocalAudioUploadContractExpect::isTrue(
|
||||
is_file($storedPath) && file_get_contents($storedPath) === $payload,
|
||||
'The internally uploaded local recording must remain byte-identical'
|
||||
);
|
||||
} finally {
|
||||
if (isset($storedPath) && is_file($storedPath)) {
|
||||
unlink($storedPath);
|
||||
}
|
||||
if (is_file($sourcePath)) {
|
||||
unlink($sourcePath);
|
||||
}
|
||||
if (is_dir($targetDir)) {
|
||||
rmdir($targetDir);
|
||||
}
|
||||
}
|
||||
|
||||
$missingPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR
|
||||
. 'zyt-missing-local-audio-' . bin2hex(random_bytes(8)) . '.webm';
|
||||
$missingRejected = false;
|
||||
try {
|
||||
$localStorage = new \app\common\service\storage\engine\Local();
|
||||
$localStorage->setUploadFileByReal($missingPath);
|
||||
} catch (Throwable $exception) {
|
||||
$missingRejected = !str_contains($exception->getMessage(), 'realPath')
|
||||
&& str_contains($exception->getMessage(), 'does not exist');
|
||||
}
|
||||
LocalAudioUploadContractExpect::isTrue(
|
||||
$missingRejected,
|
||||
'A missing merged recording must fail clearly before storage-name generation'
|
||||
);
|
||||
|
||||
echo "Local audio upload contract: OK\n";
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
use app\adminapi\lists\doctor\AppointmentLists;
|
||||
use app\adminapi\lists\tcm\DiagnosisLists;
|
||||
use app\adminapi\logic\tcm\PrescriptionLogic;
|
||||
|
||||
function prescriptionAppointmentScopeExpect(bool $condition, string $message): void
|
||||
{
|
||||
if (!$condition) {
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
}
|
||||
|
||||
function prescriptionAppointmentScopeMethod(ReflectionMethod $method): string
|
||||
{
|
||||
$lines = file($method->getFileName());
|
||||
if (!is_array($lines)) {
|
||||
throw new RuntimeException('method source must be readable');
|
||||
}
|
||||
|
||||
return implode('', array_slice(
|
||||
$lines,
|
||||
$method->getStartLine() - 1,
|
||||
$method->getEndLine() - $method->getStartLine() + 1
|
||||
));
|
||||
}
|
||||
|
||||
$diagnosisLists = prescriptionAppointmentScopeMethod(
|
||||
(new ReflectionClass(DiagnosisLists::class))->getMethod('lists')
|
||||
);
|
||||
$appointmentLists = prescriptionAppointmentScopeMethod(
|
||||
(new ReflectionClass(AppointmentLists::class))->getMethod('lists')
|
||||
);
|
||||
$getByAppointment = prescriptionAppointmentScopeMethod(
|
||||
(new ReflectionClass(PrescriptionLogic::class))->getMethod('getByAppointment')
|
||||
);
|
||||
|
||||
foreach ([$diagnosisLists, $appointmentLists] as $listSource) {
|
||||
prescriptionAppointmentScopeExpect(
|
||||
str_contains($listSource, "whereIn('appointment_id', \$appointmentIds)"),
|
||||
'list action state must be selected by the current appointment ids'
|
||||
);
|
||||
prescriptionAppointmentScopeExpect(
|
||||
str_contains($listSource, "['current_has_prescription']"),
|
||||
'list DTO must expose an appointment-scoped prescription flag'
|
||||
);
|
||||
prescriptionAppointmentScopeExpect(
|
||||
str_contains($listSource, "['current_prescription_id']"),
|
||||
'list DTO must expose the exact appointment-scoped prescription id'
|
||||
);
|
||||
}
|
||||
|
||||
prescriptionAppointmentScopeExpect(
|
||||
!str_contains(
|
||||
$diagnosisLists,
|
||||
"['prescription_audit_status'] = (\$item['has_prescription'] && \$fu)"
|
||||
),
|
||||
'diagnosis history must not drive the current appointment action state'
|
||||
);
|
||||
prescriptionAppointmentScopeExpect(
|
||||
str_contains($getByAppointment, "(int) (\$candidate->void_status ?? 0) === 0")
|
||||
&& str_contains($getByAppointment, '$row = $row ?? $fallback;'),
|
||||
'detail lookup must prefer the same active prescription and only fall back when all are voided'
|
||||
);
|
||||
|
||||
echo "Prescription appointment scope contract: OK\n";
|
||||
Reference in New Issue
Block a user