lct-55: ошибка адреса и сообщение в 112
This commit is contained in:
parent
29aca65e8c
commit
f0c9015f2b
14 changed files with 267 additions and 7 deletions
|
|
@ -678,6 +678,22 @@ class CardReply(BaseModel):
|
|||
text: str = Field(max_length=2000)
|
||||
|
||||
|
||||
class CardErrorReport112(BaseModel):
|
||||
"""Сообщение диспетчера в 112 после доклада бригады с места."""
|
||||
|
||||
type: Literal["card.error_report_112"] = "card.error_report_112"
|
||||
card_id: UUID
|
||||
text: str = Field(min_length=10, max_length=500)
|
||||
|
||||
@field_validator("text")
|
||||
@classmethod
|
||||
def meaningful_text(cls, value: str) -> str:
|
||||
value = value.strip()
|
||||
if len(value) < 10:
|
||||
raise ValueError("Укажите, что сообщено оператору 112")
|
||||
return value
|
||||
|
||||
|
||||
class CardNext(BaseModel):
|
||||
type: Literal["card.next"] = "card.next"
|
||||
card_id: UUID
|
||||
|
|
@ -721,7 +737,7 @@ ServerToStation = Annotated[
|
|||
StationToServer = Annotated[
|
||||
CardAck | CardBounce | ServiceStatusSet | CrewSelect | PhoneDial | PhoneBrief | PhoneCheck
|
||||
| PhoneHangup | PhoneReply | AdjacentDial | StationFinish
|
||||
| CardReply | CardNext | CardOpen
|
||||
| CardReply | CardErrorReport112 | CardNext | CardOpen
|
||||
| ZoneDecision | CrewDispatched | CrewArrived,
|
||||
Field(discriminator="type"),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -288,6 +288,8 @@ class StationSnapshot(BaseModel):
|
|||
phone_reports: list[PhoneReportRecord] = []
|
||||
phone_lines: list[PhoneLineRecord] = []
|
||||
phone_pending: PhoneCallPending | None = None
|
||||
card_error_discovered: bool = False
|
||||
report_112_text: str | None = None
|
||||
card_id: UUID | None = None
|
||||
card_index: int = 1
|
||||
card_total: int = 1
|
||||
|
|
|
|||
|
|
@ -5,12 +5,15 @@
|
|||
на старте приложения, а не тихо игнорироваться и всплывать посреди занятия.
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
|
||||
from app.domain.classifiers import DDSCode, IncidentType, Level, Outcome
|
||||
from app.domain.events import Mood
|
||||
from app.domain.kio import EDITABLE_KIO_FIELDS, KIO
|
||||
from app.domain.statuses import NEXT, SERVICE_STATUS_LABELS, ServiceStatus
|
||||
from app.scoring.address import address_matches
|
||||
from app.scoring.taxonomy import METRIC_MAP
|
||||
|
||||
|
||||
|
|
@ -174,6 +177,21 @@ FALLBACK_NOTIFY: dict[str, str] = {
|
|||
}
|
||||
|
||||
|
||||
class DdsCardError(Strict):
|
||||
"""Заложенное расхождение готовой карточки с эталоном для занятия ДДС."""
|
||||
|
||||
field: Literal["address"]
|
||||
card_value: str = Field(min_length=3)
|
||||
|
||||
@field_validator("card_value")
|
||||
@classmethod
|
||||
def nonempty_card_value(cls, value: str) -> str:
|
||||
value = value.strip()
|
||||
if len(value) < 3:
|
||||
raise ValueError("dds_card_error.card_value: укажите неверный адрес карточки")
|
||||
return value
|
||||
|
||||
|
||||
class Scenario(Strict):
|
||||
id: str
|
||||
title: str
|
||||
|
|
@ -202,6 +220,7 @@ class Scenario(Strict):
|
|||
# справка и передача в другой регион разбираются в lct-36.
|
||||
outcome: Outcome = Outcome.CARD
|
||||
dds_decision: DdsDecision = DdsDecision()
|
||||
dds_card_error: DdsCardError | None = None
|
||||
|
||||
# Готовая КИО, сформированная курсантом и утверждённая преподавателем.
|
||||
# Для системных сценариев поле отсутствует; ДДС использует снимок как
|
||||
|
|
@ -258,6 +277,19 @@ class Scenario(Strict):
|
|||
)
|
||||
return self
|
||||
|
||||
@model_validator(mode="after")
|
||||
def valid_dds_card_error(self):
|
||||
error = self.dds_card_error
|
||||
if error is None:
|
||||
return self
|
||||
if not self.ground_truth.address or not self.ground_truth.address.strip():
|
||||
raise ValueError("dds_card_error: для ошибки адреса нужен ground_truth.address")
|
||||
if self.student_card is not None:
|
||||
raise ValueError("dds_card_error: используйте отдельный учебный сценарий без student_card")
|
||||
if address_matches(self.ground_truth.address, error.card_value):
|
||||
raise ValueError("dds_card_error: адрес карточки должен отличаться от эталона")
|
||||
return self
|
||||
|
||||
@model_validator(mode="after")
|
||||
def unique_reference_ids(self):
|
||||
for label, values in (
|
||||
|
|
|
|||
|
|
@ -99,6 +99,11 @@ def to_csv(report: SessionReport) -> bytes:
|
|||
row("Сверка самооценки", "", label, ", ".join(values))
|
||||
for number, item in enumerate(report.notes, 1):
|
||||
row("Пометки преподавателя", number, item.transcript_ref, item.text, item.author)
|
||||
for card_number, card in enumerate(report.card_results, 1):
|
||||
for action in card.actions:
|
||||
if action.get("type") == "card.error_report_112":
|
||||
row("Сообщение в 112", card_number, "Ошибка карточки",
|
||||
action.get("text", ""), action.get("at", ""))
|
||||
return ("\N{ZERO WIDTH NO-BREAK SPACE}" + output.getvalue()).encode("utf-8")
|
||||
|
||||
|
||||
|
|
@ -259,6 +264,17 @@ def to_pdf(report: SessionReport) -> bytes:
|
|||
for item in report.notes:
|
||||
story.append(p(f"{item.transcript_ref} - {item.author}: {item.text}"))
|
||||
|
||||
messages_112 = [
|
||||
(number, action)
|
||||
for number, card in enumerate(report.card_results, 1)
|
||||
for action in card.actions
|
||||
if action.get("type") == "card.error_report_112"
|
||||
]
|
||||
if messages_112:
|
||||
section("Сообщения в 112 об ошибках карточек")
|
||||
for number, action in messages_112:
|
||||
story.append(p(f"Карточка {number}, {action.get('at', '')}: {action.get('text', '')}"))
|
||||
|
||||
output = io.BytesIO()
|
||||
document = SimpleDocTemplate(
|
||||
output, pagesize=A4, leftMargin=42, rightMargin=42,
|
||||
|
|
|
|||
|
|
@ -195,6 +195,9 @@ def build_card(
|
|||
)
|
||||
if service:
|
||||
kio = apply_patch(kio, {"dds": service})
|
||||
if scenario.dds_card_error is not None:
|
||||
# Ошибка относится к выданному снимку; эталон сценария остаётся верным.
|
||||
kio = kio.model_copy(update={"address": scenario.dds_card_error.card_value})
|
||||
kio.response_status = ResponseStatus.TRANSFERRED
|
||||
timers = SessionTimers(limits=dict(limits))
|
||||
timers.on_event("dds.dispatch")
|
||||
|
|
@ -401,7 +404,14 @@ def _deliver_card_adjacent(state: "SessionState", card: DdsLiveCard, now: dateti
|
|||
def _finish_phone_call(card: DdsLiveCard) -> list[Any]:
|
||||
call = card.phone_pending
|
||||
assert call is not None
|
||||
text = _report_text(call.phase, call.crew, card.dispatched_card.address or "из карточки")
|
||||
error = card.scenario.dds_card_error
|
||||
if call.phase == "arrived" and error is not None and error.field == "address":
|
||||
actual = card.scenario.ground_truth.address
|
||||
text = (f"{call.crew}: прибыли по адресу {actual}. "
|
||||
f"В карточке указан {card.dispatched_card.address}; "
|
||||
"адрес не совпадает. Сообщите об ошибке в 112.")
|
||||
else:
|
||||
text = _report_text(call.phase, call.crew, card.dispatched_card.address or "из карточки")
|
||||
line = _line(card, "crew", text)
|
||||
report = PhoneReportRecord(service=call.service, crew=call.crew,
|
||||
phase=call.phase, text=text, at=now_utc())
|
||||
|
|
@ -749,6 +759,16 @@ class DdsDesk(BaseModel):
|
|||
return DeskOutcome()
|
||||
card.reply_text = command.text
|
||||
card.reply_log.append((now_utc(), command.text))
|
||||
case "card.error_report_112":
|
||||
if command.card_id != card.card_id:
|
||||
return _refused("Сообщение относится не к текущей карточке")
|
||||
if card.scenario.dds_card_error is None or not any(
|
||||
report.phase == "arrived" for report in card.phone_reports
|
||||
):
|
||||
return _refused("Сначала получите доклад бригады с места об ошибке карточки")
|
||||
if any(action == "card.error_report_112" for action, _at, _detail in card.dds_log):
|
||||
return DeskOutcome()
|
||||
card.dds_log.append(("card.error_report_112", now_utc(), command.text.strip()))
|
||||
case "card.open":
|
||||
if not dds_lesson or not self.open(command.card_id):
|
||||
return _refused("Карточка отсутствует в текущей очереди")
|
||||
|
|
|
|||
|
|
@ -436,6 +436,13 @@ class SessionState(PersistedSession):
|
|||
phone_reports=list(card.phone_reports) if card is not None else [],
|
||||
phone_lines=list(card.phone_lines) if card is not None else [],
|
||||
phone_pending=card.phone_pending if card is not None else None,
|
||||
card_error_discovered=(card.scenario.dds_card_error is not None and any(
|
||||
report.phase == "arrived" for report in card.phone_reports
|
||||
)) if card is not None else False,
|
||||
report_112_text=(next(
|
||||
(detail for action, _at, detail in reversed(card.dds_log)
|
||||
if action == "card.error_report_112"), None,
|
||||
) if card is not None else None),
|
||||
card_id=card.card_id if card is not None else None,
|
||||
card_index=(desk.card_index + 1 if card is not None
|
||||
else min(desk.next_index + 1, len(desk.scenarios))),
|
||||
|
|
|
|||
Loading…
Reference in a new issue