(int) ($row[$field] ?? 0) === $value ))); } } final class AppointmentCallPolicyFixtureQuery { public function __construct(private array $rows) {} public function field(array $fields): self { return $this; } public function select(): self { return $this; } public function toArray(): array { return $this->rows; } } class_alias(AppointmentCallPolicyFixtureModel::class, 'app\\common\\model\\doctor\\Appointment'); $assertions = 0; function callPolicyExpect(bool $condition, string $message): void { global $assertions; $assertions++; if (!$condition) { throw new RuntimeException($message); } } function callPolicyRow(int $id, ?string $type, int $status = 1, string $date = '2026-09-09', string $time = '09:00:00'): array { return ['id' => $id, 'patient_id' => 101, 'appointment_type' => $type, 'status' => $status, 'appointment_date' => $date, 'appointment_time' => $time]; } function callPolicyRejects(callable $callback, string $messagePart): void { try { $callback(); } catch (RuntimeException $error) { callPolicyExpect(str_contains($error->getMessage(), $messagePart), 'rejection explains ' . $messagePart); return; } throw new RuntimeException('Expected rejection: ' . $messagePart); } $video = AppointmentCallPolicy::policyForAppointment(callPolicyRow(1, 'video')); callPolicyExpect($video['appointment_type'] === 'video' && $video['appointment_type_desc'] === '视频问诊', 'video retains accurate type and label'); callPolicyExpect($video['can_video_call'] && $video['can_audio_call'] && $video['call_disabled_reason'] === '', 'video enables both media'); $text = AppointmentCallPolicy::policyForAppointment(callPolicyRow(2, 'text')); callPolicyExpect(!$text['can_video_call'] && !$text['can_audio_call'] && $text['appointment_type_desc'] === '图文问诊', 'text enables neither video nor audio'); $phone = AppointmentCallPolicy::policyForAppointment(callPolicyRow(3, 'phone')); callPolicyExpect(!$phone['can_video_call'] && $phone['can_audio_call'] && $phone['appointment_type_desc'] === '电话问诊', 'legacy phone allows audio only'); foreach (['offline', 'unknown', 'Video', ' video '] as $type) { $policy = AppointmentCallPolicy::policyForAppointment(callPolicyRow(4, $type)); callPolicyExpect($policy['appointment_type'] === $type && !$policy['can_video_call'] && !$policy['can_audio_call'], 'unsupported stored type never becomes video: ' . $type); } foreach ([null, '', ' '] as $type) { $policy = AppointmentCallPolicy::policyForAppointment(callPolicyRow(5, $type)); callPolicyExpect($policy['appointment_type'] === 'video' && $policy['can_video_call'], 'a real historical blank registration follows existing video normalization'); } foreach (['video', 'text', 'phone', null] as $type) { $policy = AppointmentCallPolicy::policyForAppointment(callPolicyRow(6, $type, 2)); callPolicyExpect(!$policy['can_video_call'] && !$policy['can_audio_call'] && str_contains($policy['call_disabled_reason'], '已取消'), 'canceled registration never enables calls'); } $invalidStatus = AppointmentCallPolicy::policyForAppointment(callPolicyRow(7, 'video', 0)); callPolicyExpect(!$invalidStatus['can_video_call'] && !$invalidStatus['can_audio_call'], 'unrecognized status cannot grant calls'); $none = AppointmentCallPolicy::resolveFromAppointments([]); callPolicyExpect($none === [ 'appointment_id' => 0, 'appointment_type' => null, 'appointment_type_desc' => '未挂号', 'can_video_call' => false, 'can_audio_call' => false, 'call_disabled_reason' => '未挂号,不能发起通话', ], 'missing registration never fabricates video eligibility'); $mixed = [callPolicyRow(10, 'text'), callPolicyRow(11, 'video')]; $ambiguous = AppointmentCallPolicy::resolveFromAppointments($mixed); callPolicyExpect($ambiguous['appointment_id'] === 0 && $ambiguous['appointment_type'] === null, 'multiple active registrations do not guess identity or mode'); callPolicyExpect(!$ambiguous['can_video_call'] && !$ambiguous['can_audio_call'] && $ambiguous['call_disabled_reason'] === '请指定本次挂号', 'multiple active registrations require an explicit choice'); callPolicyExpect(AppointmentCallPolicy::resolveFromAppointments($mixed, 10)['appointment_type'] === 'text', 'explicit text choice is not replaced by video'); callPolicyExpect(AppointmentCallPolicy::resolveFromAppointments($mixed, 11)['can_video_call'], 'explicit video choice is permitted'); $sameMode = AppointmentCallPolicy::resolveFromAppointments([callPolicyRow(10, 'video'), callPolicyRow(11, 'video')]); callPolicyExpect(!$sameMode['can_video_call'], 'multiple registrations still require a choice when their modes match'); callPolicyRejects(static fn () => AppointmentCallPolicy::resolveFromAppointments($mixed, 99), '不属于当前诊单'); callPolicyRejects(static fn () => AppointmentCallPolicy::resolveFromAppointments($mixed, -1), '不属于当前诊单'); $singleActive = AppointmentCallPolicy::resolveFromAppointments([ callPolicyRow(20, 'text', 1, '2026-08-01'), callPolicyRow(21, 'video', 3, '2026-09-09'), callPolicyRow(22, 'video', 2, '2026-09-10'), ]); callPolicyExpect($singleActive['appointment_id'] === 20 && !$singleActive['can_video_call'], 'single active registration wins over newer history and cancellation'); $history = [ callPolicyRow(101, 'video', 3, '2026-09-08', '23:00'), callPolicyRow(30, 'phone', 4, '2026-09-09', '09:00'), callPolicyRow(32, 'text', 3, '2026-09-09', '9:30'), callPolicyRow(31, 'video', 3, '2026-09-09', '09:30:00'), callPolicyRow(33, 'video', 2, '2026-09-10', '10:00:00'), ]; $latest = AppointmentCallPolicy::resolveFromAppointments($history); callPolicyExpect($latest['appointment_id'] === 32 && $latest['appointment_type'] === 'text' && !$latest['can_video_call'], 'history uses date then normalized time then id, ignoring canceled entries'); $explicitCanceled = AppointmentCallPolicy::resolveFromAppointments($history, 33); callPolicyExpect($explicitCanceled['appointment_id'] === 33 && $explicitCanceled['appointment_type'] === 'video' && !$explicitCanceled['can_video_call'], 'explicit canceled choice preserves identity and accurate type but denies calls'); callPolicyExpect(AppointmentCallPolicy::resolveFromAppointments([callPolicyRow(40, 'video', 2)])['appointment_type'] === null, 'canceled-only history does not invent an eligible registration'); foreach ([3, 4] as $status) { callPolicyExpect(AppointmentCallPolicy::resolveFromAppointments([callPolicyRow(41, 'video', $status)])['can_video_call'], 'completed/missed history remains usable for legacy chat'); } AppointmentCallPolicyFixtureModel::$rows = [ callPolicyRow(50, 'text'), array_replace(callPolicyRow(99, 'video'), ['patient_id' => 202]), ]; $resolved = AppointmentCallPolicy::resolve(101, 50); callPolicyExpect(AppointmentCallPolicyFixtureModel::$lastWhere === ['patient_id', 101], 'model query scopes appointment.patient_id to diagnosis id'); callPolicyExpect($resolved['appointment_id'] === 50 && !$resolved['can_video_call'], 'scoped resolve uses the requested registration'); callPolicyRejects(static fn () => AppointmentCallPolicy::resolve(101, 99), '不属于当前诊单'); callPolicyRejects(static fn () => AppointmentCallPolicy::resolve(0), '诊单 ID 无效'); echo "Appointment call policy: {$assertions} assertions passed (in-memory model, no database)\n";