Files
zyt/server/tests/RevisitDueCalculatorTest.php
T
2026-10-22 11:12:16 +08:00

236 lines
16 KiB
PHP
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
/**
* 复诊到期计算器(RevisitDueCalculator)纯函数单测,不连库。
* php server/tests/RevisitDueCalculatorTest.php
* 覆盖:锚点回退链三种、天数回落三种、15 天不归 14 档、computeFacts 失效/挂号选择、
* deriveState 九态与优先级(missed 优先于 overdue 且不受 due_date 限制、contacted、warning/pending 边界、no_days、none)、
* 人工关闭文案、escalationLevel、日期工具。
*/
require dirname(__DIR__) . '/vendor/autoload.php';
use app\common\service\tcm\RevisitDueCalculator as C;
$checks = 0;
function expect(bool $condition, string $message): void
{
global $checks;
if (!$condition) {
fwrite(STDERR, "FAIL: {$message}\n");
exit(1);
}
$checks++;
}
$cfg = C::defaultConfig();
$ts = static fn (string $dt): int => (int) strtotime($dt);
/** 基础输入:订单 09-20 10:00 下单、28 天 */
$baseInput = static function (array $override = []) use ($ts): array {
return array_replace([
'diagnosis_id' => 1,
'assistant_id' => 7,
'patient_name' => '张三',
'latest_order' => ['id' => 100, 'create_time' => $ts('2026-09-20 10:00:00'), 'medication_days' => 28, 'prescription_id' => 55],
'valid_order_count' => 3,
'offset' => 0,
'rx_usage_days' => null,
'sign_ts' => 0,
'ship_ts' => 0,
'appointments' => [],
'first_touch_ts' => $ts('2026-03-02 09:00:00'),
'total_medication_days' => 84,
], $override);
};
// ---------- 锚点回退链 ----------
$f = C::computeFacts($baseInput(['sign_ts' => $ts('2026-09-22 15:30:00'), 'ship_ts' => $ts('2026-09-21 08:00:00')]), $cfg, '2026-10-01');
expect($f['anchor_src'] === C::ANCHOR_SIGN && $f['anchor_ts'] === $ts('2026-09-22 15:30:00'), '锚点:有签收优先签收');
expect($f['due_date'] === '2026-10-20', '锚点签收 09-22 + 28 天 = 10-20');
$f = C::computeFacts($baseInput(['ship_ts' => $ts('2026-09-21 08:00:00')]), $cfg, '2026-10-01');
expect($f['anchor_src'] === C::ANCHOR_SHIP && $f['due_date'] === '2026-10-19', '锚点:无签收回落发货 09-21 → 10-19');
$f = C::computeFacts($baseInput(), $cfg, '2026-10-01');
expect($f['anchor_src'] === C::ANCHOR_CREATE && $f['due_date'] === '2026-10-18', '锚点:都没有用下单日 09-20 → 10-18');
$f = C::computeFacts($baseInput(['sign_ts' => $ts('2026-09-22 15:30:00')]), ['anchor_chain' => ['create']], '2026-10-01');
expect($f['anchor_src'] === C::ANCHOR_CREATE, 'anchor_chain 收窄为 create 时忽略签收');
$f = C::computeFacts($baseInput(['sign_ts' => $ts('2026-09-22 15:30:00')]), ['anchor_chain' => ['ship']], '2026-10-01');
expect($f['anchor_src'] === C::ANCHOR_CREATE, '链耗尽无结果时兜底下单时间');
$f = C::computeFacts($baseInput(['sign_ts' => $ts('2026-09-22 15:30:00')]), ['anchor_chain' => 'ship,sign'], '2026-10-01');
expect($f['anchor_src'] === C::ANCHOR_SIGN, 'anchor_chain 支持逗号字符串');
// ---------- 天数回落 ----------
$f = C::computeFacts($baseInput(['rx_usage_days' => 14]), $cfg, '2026-10-01');
expect($f['medication_days'] === 28 && $f['days_src'] === C::DAYS_SRC_ORDER, '天数:订单有值用订单');
$f = C::computeFacts($baseInput(['latest_order' => ['id' => 100, 'create_time' => $ts('2026-09-20 10:00:00'), 'medication_days' => 0, 'prescription_id' => 55], 'rx_usage_days' => 14]), $cfg, '2026-10-01');
expect($f['medication_days'] === 14 && $f['days_src'] === C::DAYS_SRC_RX && $f['due_date'] === '2026-10-04', '天数:订单缺失回落处方 14');
$f = C::computeFacts($baseInput(['latest_order' => ['id' => 100, 'create_time' => $ts('2026-09-20 10:00:00'), 'medication_days' => null, 'prescription_id' => 55], 'rx_usage_days' => null]), $cfg, '2026-10-01');
expect($f['medication_days'] === 0 && $f['days_src'] === C::DAYS_SRC_NONE && $f['due_date'] === null, '天数:都缺失 → 0 且无 due_date');
expect($f['closed_state_auto'] === C::CLOSED_OPEN, '缺天数仍为进行中(由读取侧推导 no_days)');
$f = C::computeFacts($baseInput(['latest_order' => ['id' => 100, 'create_time' => $ts('2026-09-20 10:00:00'), 'medication_days' => 0, 'prescription_id' => 55], 'rx_usage_days' => 14]), ['days_fallback_rx' => false], '2026-10-01');
expect($f['medication_days'] === 0, 'days_fallback_rx=false 不回落处方');
$f = C::computeFacts($baseInput(['latest_order' => ['id' => 100, 'create_time' => $ts('2026-09-20 10:00:00'), 'medication_days' => 0, 'prescription_id' => 0]]), ['days_default' => 7], '2026-10-01');
expect($f['medication_days'] === 7 && $f['days_src'] === C::DAYS_SRC_NONE && $f['due_date'] === '2026-09-27', 'days_default>0 时兜底天数且 days_src 仍为 0');
// ---------- 档位 ----------
$f = C::computeFacts($baseInput(['latest_order' => ['id' => 100, 'create_time' => $ts('2026-09-20 10:00:00'), 'medication_days' => 15, 'prescription_id' => 55]]), $cfg, '2026-10-01');
expect($f['rule_days'] === 0 && $f['due_date'] === '2026-10-05', '15 天不归 14 档,到期仍按实际天数');
$f = C::computeFacts($baseInput(), $cfg, '2026-10-01');
expect($f['rule_days'] === 28, '28 天命中档位');
$f = C::computeFacts($baseInput(), ['grace_days' => 2], '2026-10-01');
expect($f['due_date'] === '2026-10-20', 'grace_days 顺延');
// ---------- 诊次 / 首次触达 / 失效 ----------
$f = C::computeFacts($baseInput(['offset' => 2]), $cfg, '2026-10-01');
expect($f['current_slot'] === 5 && $f['next_slot'] === 6, 'current_slot = 有效单 3 + 偏移 2');
expect($f['first_touch_date'] === '2026-03-02', 'first_touch_date 由时间戳转日期');
$f = C::computeFacts($baseInput(['offset' => 99]), $cfg, '2026-10-01');
expect($f['current_slot'] === 23, '偏移钳到 20');
$f = C::computeFacts($baseInput(['latest_order' => null, 'valid_order_count' => 0]), $cfg, '2026-10-01');
expect($f['closed_state_auto'] === C::CLOSED_INVALID && $f['source_order_id'] === 0 && $f['due_date'] === null, '无有效订单 → 失效');
$f = C::computeFacts($baseInput(['assistant_id' => 0]), $cfg, '2026-10-01');
expect($f['closed_state_auto'] === C::CLOSED_INVALID, '无医助 → 失效');
$f = C::computeFacts($baseInput(['diagnosis_active' => false]), $cfg, '2026-10-01');
expect($f['closed_state_auto'] === C::CLOSED_INVALID, '诊单删除/禁用 → 失效');
// ---------- 挂号选择 ----------
$appts = [
['id' => 1, 'status' => 3, 'appointment_date' => '2026-09-19', 'appointment_time' => '10:00:00'], // 订单日之前,忽略
['id' => 2, 'status' => 2, 'appointment_date' => '2026-10-21', 'appointment_time' => '10:00:00'], // 已取消,忽略
['id' => 3, 'status' => 1, 'appointment_date' => '2026-10-23', 'appointment_time' => ''],
['id' => 4, 'status' => 1, 'appointment_date' => '2026-10-23', 'appointment_time' => '09:30:00'],
];
$f = C::computeFacts($baseInput(['appointments' => $appts]), $cfg, '2026-10-21');
expect($f['appointment_state'] === 1 && $f['appointment_date'] === '2026-10-23' && $f['booked_appointment_id'] === 4, '挂号取锚点日之后最新一条(空时间视为 00:00:00,排在 09:30 之前)');
expect($f['closed_state_auto'] === C::CLOSED_BOOKED, '已约且日期未过 → 自动关闭 2');
$f = C::computeFacts($baseInput(['appointments' => $appts]), $cfg, '2026-10-24');
expect($f['appointment_state'] === 1 && $f['closed_state_auto'] === C::CLOSED_OPEN, '已约但日期已过(cron 未跑)→ 进行中');
$f = C::computeFacts($baseInput(['appointments' => [['id' => 9, 'status' => 3, 'appointment_date' => '2026-10-23', 'appointment_time' => '10:00:00']]]), $cfg, '2026-10-24');
expect($f['appointment_state'] === 3 && $f['closed_state_auto'] === C::CLOSED_VISITED && $f['booked_appointment_id'] === 9, '已面诊 → 自动关闭 3');
$f = C::computeFacts($baseInput(['appointments' => [['id' => 9, 'status' => 4, 'appointment_date' => '2026-10-23', 'appointment_time' => '10:00:00']]]), $cfg, '2026-10-23');
expect($f['appointment_state'] === 4 && $f['closed_state_auto'] === C::CLOSED_OPEN, '过号 → 进行中');
// 下单当天(及更早)的面诊是产生这笔药的那次面诊,不算复诊;次日起才算(appointment_min_gap_days 默认 1)
$f = C::computeFacts($baseInput(['appointments' => [['id' => 11, 'status' => 3, 'appointment_date' => '2026-09-20', 'appointment_time' => '09:30:00']]]), $cfg, '2026-09-21');
expect($f['appointment_state'] === 0 && $f['closed_state_auto'] === C::CLOSED_OPEN && $f['booked_appointment_id'] === 0, '下单当天的已面诊不计入,周期保持进行中');
$f = C::computeFacts($baseInput(['appointments' => [['id' => 12, 'status' => 3, 'appointment_date' => '2026-09-21', 'appointment_time' => '09:30:00']]]), $cfg, '2026-09-22');
expect($f['appointment_state'] === 3 && $f['closed_state_auto'] === C::CLOSED_VISITED, '下单次日的已面诊计入 → 自动关闭 3');
$f = C::computeFacts($baseInput(['appointments' => [['id' => 12, 'status' => 3, 'appointment_date' => '2026-09-21', 'appointment_time' => '09:30:00']]]), ['appointment_min_gap_days' => 2] + $cfg, '2026-09-22');
expect($f['appointment_state'] === 0, 'appointment_min_gap_days=2 时下单次日的面诊也不计入');
// ---------- deriveState ----------
$row = static function (array $override = []): array {
return array_replace([
'closed_state' => 0,
'assistant_id' => 7,
'source_order_id' => 100,
'valid_order_count' => 3,
'medication_days' => 28,
'due_date' => '2026-10-20',
'appointment_state' => 0,
'appointment_date' => null,
'promised_date' => null,
'snooze_count' => 0,
], $override);
};
$s = C::deriveState($row(['appointment_state' => 3, 'appointment_date' => '2026-10-23']), '2026-10-30', $cfg);
expect($s['state'] === C::STATE_VISITED && $s['state_text'] === '已面诊 10-23' && $s['severe'] === 0, 'visited 优先且不深红');
$s = C::deriveState($row(['appointment_state' => 1, 'appointment_date' => '2026-10-23']), '2026-10-21', $cfg);
expect($s['state'] === C::STATE_BOOKED && $s['state_text'] === '已约 10-23', 'booked');
$s = C::deriveState($row(['appointment_state' => 1, 'appointment_date' => '2026-10-21']), '2026-10-21', $cfg);
expect($s['state'] === C::STATE_BOOKED, 'booked:预约日 = today 仍为已约');
$s = C::deriveState($row(['appointment_state' => 4, 'appointment_date' => '2026-10-23']), '2026-10-23', $cfg);
expect($s['state'] === C::STATE_MISSED && $s['state_text'] === '过号 10-23', 'missed:status=4');
$s = C::deriveState($row(['appointment_state' => 1, 'appointment_date' => '2026-10-22']), '2026-10-23', $cfg);
expect($s['state'] === C::STATE_MISSED, 'missed:已约日期已过(cron 未跑兜底)');
$s = C::deriveState($row(['appointment_state' => 4, 'appointment_date' => '2026-10-02', 'due_date' => '2026-10-20']), '2026-10-02', $cfg);
expect($s['state'] === C::STATE_MISSED && $s['days_left'] === 18 && $s['overdue_days'] === 0, 'missed 不受 due_date 限制(到期前过号同样标红)');
$s = C::deriveState($row(['appointment_state' => 4, 'appointment_date' => '2026-10-23', 'promised_date' => '2026-10-30']), '2026-10-23', $cfg);
expect($s['state'] === C::STATE_MISSED, 'missed 优先于 contacted');
$s = C::deriveState($row(['appointment_state' => 4, 'appointment_date' => '2026-10-28']), '2026-10-28', $cfg);
expect($s['state'] === C::STATE_MISSED && $s['overdue_days'] === 8 && $s['severe'] === 1, 'missed 且超期 ≥7 天 → 深红');
$s = C::deriveState($row(['promised_date' => '2026-10-25']), '2026-10-22', $cfg);
expect($s['state'] === C::STATE_CONTACTED && $s['state_text'] === '已联系·约 10-25' && $s['severe'] === 0, 'contacted 覆盖 overdue');
$s = C::deriveState($row(['promised_date' => '2026-10-22']), '2026-10-22', $cfg);
expect($s['state'] === C::STATE_CONTACTED, 'contacted:承诺日 = today 仍为已联系');
$s = C::deriveState($row(['promised_date' => '2026-10-21']), '2026-10-22', $cfg);
expect($s['state'] === C::STATE_OVERDUE, '承诺日已过 → 回到 overdue');
$s = C::deriveState($row(), '2026-10-20', $cfg);
expect($s['state'] === C::STATE_OVERDUE && $s['state_text'] === '今日应挂号' && $s['overdue_days'] === 0 && $s['days_left'] === 0 && $s['severe'] === 0, 'overdue:到期日当天');
$s = C::deriveState($row(), '2026-10-26', $cfg);
expect($s['state'] === C::STATE_OVERDUE && $s['state_text'] === '超期 6 天' && $s['severe'] === 0, 'overdue 6 天未深红');
$s = C::deriveState($row(), '2026-10-27', $cfg);
expect($s['state'] === C::STATE_OVERDUE && $s['overdue_days'] === 7 && $s['severe'] === 1, 'overdue 7 天深红');
$s = C::deriveState($row(), '2026-10-27', ['severe_overdue_days' => [14, 21, 28, 30]]);
expect($s['severe'] === 0, 'severe 阶梯数组取最小阈值 14:7 天不深红');
$s = C::deriveState($row(), '2026-11-03', ['severe_overdue_days' => [14, 21, 28, 30]]);
expect($s['severe'] === 1, 'severe 阶梯数组:14 天深红');
$s = C::deriveState($row(), '2026-10-19', $cfg);
expect($s['state'] === C::STATE_WARNING && $s['state_text'] === '剩 1 天' && $s['days_left'] === 1, 'warning:剩 1 天');
$s = C::deriveState($row(), '2026-10-17', $cfg);
expect($s['state'] === C::STATE_WARNING && $s['days_left'] === 3, 'warning 边界:剩 3 天 = warn_days');
$s = C::deriveState($row(), '2026-10-16', $cfg);
expect($s['state'] === C::STATE_PENDING && $s['state_text'] === '剩 4 天' && $s['overdue_days'] === 0, 'pending 边界:剩 4 天');
$s = C::deriveState($row(), '2026-10-16', ['warn_days' => 5]);
expect($s['state'] === C::STATE_WARNING, 'warn_days 可配');
$s = C::deriveState($row(['due_date' => null, 'medication_days' => 0]), '2026-10-16', $cfg);
expect($s['state'] === C::STATE_NO_DAYS && $s['state_text'] === '缺开药天数' && $s['days_left'] === null && $s['overdue_days'] === null, 'no_days');
$s = C::deriveState($row(['due_date' => '', 'medication_days' => 0, 'appointment_state' => 4, 'appointment_date' => '2026-10-10']), '2026-10-10', $cfg);
expect($s['state'] === C::STATE_MISSED, '缺天数但过号 → missed 优先');
$s = C::deriveState($row(['valid_order_count' => 0, 'source_order_id' => 0]), '2026-10-16', $cfg);
expect($s['state'] === C::STATE_NONE && $s['state_text'] === '' && $s['days_left'] === null, 'none:无有效订单');
$s = C::deriveState($row(['assistant_id' => 0]), '2026-10-16', $cfg);
expect($s['state'] === C::STATE_NONE, 'none:无医助');
$s = C::deriveState($row(['closed_state' => 5]), '2026-10-16', $cfg);
expect($s['state'] === C::STATE_NONE, 'none:closed_state=5');
$s = C::deriveState($row(['closed_state' => 4]), '2026-10-27', $cfg);
expect($s['state'] === C::STATE_OVERDUE && $s['state_text'] === '已关闭·超期 7 天' && $s['severe'] === 0, '人工关闭保留事实状态、前缀文案、不深红');
$s = C::deriveState($row(['closed_state' => 4, 'appointment_state' => 3, 'appointment_date' => '2026-10-23']), '2026-10-27', $cfg);
expect($s['state'] === C::STATE_VISITED && $s['state_text'] === '已面诊 10-23', '人工关闭后已面诊文案不加前缀');
$s = C::deriveState($row(['closed_state' => 2, 'appointment_state' => 1, 'appointment_date' => '2026-10-23']), '2026-10-24', $cfg);
expect($s['state'] === C::STATE_MISSED, 'closed_state=2 但预约日已过 → 事实推导 missed');
// ---------- escalationLevel ----------
expect(C::escalationLevel(0) === 0, 'level 0');
expect(C::escalationLevel(1) === 1 && C::escalationLevel(2) === 1, 'level 1 (1~2)');
expect(C::escalationLevel(3) === 2 && C::escalationLevel(4) === 2, 'level 2 (3~4)');
expect(C::escalationLevel(5) === 3 && C::escalationLevel(99) === 3, 'level 3 (>=5)');
expect(C::escalationLevel(-1) === 0, '负数视为 0');
// ---------- 日期工具 ----------
expect(C::dayDiff('2026-10-20', '2026-10-27') === 7 && C::dayDiff('2026-10-27', '2026-10-20') === -7 && C::dayDiff('2026-10-20', '2026-10-20') === 0, 'dayDiff 正负零');
expect(C::dayDiff('2026-02-27', '2026-03-02') === 3, 'dayDiff 跨月(2026 非闰年)');
expect(C::addDays('2026-09-22', 28) === '2026-10-20' && C::addDays('2026-12-31', 1) === '2027-01-01' && C::addDays('2026-01-01', -1) === '2025-12-31', 'addDays 跨月跨年');
expect(C::dayDiff('bad', '2026-10-20') === 0 && C::addDays('bad', 3) === 'bad', '非法日期容错');
// ---------- normalizeConfig ----------
$n = C::normalizeConfig(['anchor_chain' => ['SIGN', 'x', 'create', 'create'], 'day_buckets' => ['14', 21, 0], 'warn_days' => '2']);
expect($n['anchor_chain'] === ['sign', 'create'] && $n['day_buckets'] === [14, 21] && $n['warn_days'] === 2, 'normalizeConfig 归一');
expect($n['severe_overdue_days'] === 7 && $n['days_fallback_rx'] === true, 'normalizeConfig 补默认值');
echo "RevisitDueCalculatorTest OK ({$checks} checks)\n";