Files
zyt/server/tests/AppointmentCallPolicyTest.php
T
2026-09-09 15:47:48 +08:00

140 lines
8.0 KiB
PHP

<?php
declare(strict_types=1);
// 只加载纯策略与枚举,模型使用内存替身;不启动框架或读取数据库配置。
require dirname(__DIR__) . '/app/common/enum/AppointmentTypeEnum.php';
require dirname(__DIR__) . '/app/common/service/AppointmentCallPolicy.php';
use app\common\service\AppointmentCallPolicy;
final class AppointmentCallPolicyFixtureModel
{
public static array $rows = [];
public static array $lastWhere = [];
public static function where(string $field, int $value): AppointmentCallPolicyFixtureQuery
{
self::$lastWhere = [$field, $value];
return new AppointmentCallPolicyFixtureQuery(array_values(array_filter(
self::$rows,
static fn (array $row): bool => (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";