"""Одновременная выдача учебных карточек ДДС в рамках одного занятия.""" import re from datetime import datetime, timedelta from app.domain import ekp from app.domain.kio import KIO, ResponseStatus from app.scenarios.schema import Scenario from app.session.state import DdsLiveCard, SessionState, now_utc from app.session.timers import SessionTimers def prepare_card(state: SessionState, scenario: Scenario) -> None: """Сменить активную карточку, сохранив общий ID занятия и прошлые результаты.""" state.scenario = scenario.model_copy(deep=True) state.scenario_id = scenario.id state.scenario_title = scenario.title state.level = scenario.level.value state.required_fields = list(scenario.required_fields) truth = scenario.ground_truth address_fact = next((fact.value for fact in scenario.facts if "address" in fact.id), "") caller_fact = next((fact.value for fact in scenario.facts if fact.id in {"f_caller", "f_applicant"}), "") caller_phone = next(( match.group(0).strip() for match in re.finditer(r"(? DdsLiveCard: state.dds_card_index = index prepare_card(state, scenario) card = DdsLiveCard( original_index=index, scenario=state.scenario, kio=state.kio, dispatched_card=state.dispatched_card, dispatched_at=state.dispatched_at, timers=state.timers, bounced_fields=state.bounced_fields, dds_log=state.dds_log, status_log=state.status_log, crew_selected=state.crew_selected, crew_assignments=state.crew_assignments, phone_reports=state.phone_reports, phone_lines=state.phone_lines, phone_pending=state.phone_pending, reply_text=state.reply_text, reply_grammar=state.reply_grammar, reply_log=state.reply_log, ) state.dds_live_cards.append(card) return card def deliver_due_cards(state: SessionState, now: datetime | None = None) -> int: """Выдать всё, чему настал срок и для чего есть место в ожидании. Активная карточка не считается ожидающей. Каждый вызов prepare_card запускает собственный 30-секундный таймер ровно при поступлении. """ now = now or now_utc() if state.dds_next_arrival_at is None or state.dds_next_arrival_at > now: return 0 delivered = 0 while state.dds_next_scenario_index < len(state.dds_scenarios): active_id = state.dds_active_card_id if any( item.card_id == state.dds_active_card_id for item in state.dds_live_cards ) else None active_exists = active_id is not None waiting_count = len(state.dds_live_cards) - int(active_exists) if waiting_count >= state.dds_max_waiting: break index = state.dds_next_scenario_index card = _append_live_card(state, state.dds_scenarios[index], index) state.dds_next_scenario_index += 1 delivered += 1 if not active_exists: state.activate_dds_card(card.card_id, capture=False) else: # prepare_card builds the new snapshot through SessionState's # working fields; restore the trainee's active card immediately. state.activate_dds_card(active_id, capture=False) if state.dds_next_scenario_index >= len(state.dds_scenarios): state.dds_next_arrival_at = None break if state.dds_arrival_interval_seconds: # Pace from actual delivery time. If the cap was full, the next # card becomes due one full interval after this release. state.dds_next_arrival_at = card.dispatched_at + timedelta( seconds=state.dds_arrival_interval_seconds ) break # Zero interval means fill active + available waiting slots now. state.dds_next_arrival_at = now return delivered def prepare_queue( state: SessionState, scenarios: list[Scenario], arrival_interval_seconds: int = 0, max_waiting: int = 3, ) -> None: """Начать поступление очереди с первой карточки; остальные идут по темпу.""" state.dds_live_cards.clear() state.dds_scenarios = scenarios state.dds_arrival_interval_seconds = arrival_interval_seconds state.dds_max_waiting = max_waiting state.dds_next_scenario_index = 0 state.dds_next_arrival_at = now_utc() deliver_due_cards(state)