Files
zyt/server/tests/RevisitDueCalculatorTest.php
T
2026-10-15 11:03:54 +08:00

236 lines
16 KiB
PHP
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";