diff --git a/docs/ats/ss-dialog.agi b/docs/ats/ss-dialog.agi index 4fca788..eeda485 100644 --- a/docs/ats/ss-dialog.agi +++ b/docs/ats/ss-dialog.agi @@ -84,6 +84,7 @@ send_result () { \"customer_phone\":\"${CUSTOMER_PHONE:-}\", \"attempt_no\":${ATTEMPT:-1}, \"scenario\":\"$SCENARIO\", + \"call_type\":\"${TYPE:-dlv}\", \"call_started_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\", \"answered_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\", \"duration_sec\":0, diff --git a/scripts/autocall_scheduler.py b/scripts/autocall_scheduler.py index 3398339..794a803 100644 --- a/scripts/autocall_scheduler.py +++ b/scripts/autocall_scheduler.py @@ -69,7 +69,7 @@ def load_settings(conn): "delay_after_sms_minutes": 90, "retry_interval_minutes": 120, "attempts_per_day": 3, "max_attempts": 5, "no_input_escalate_after": 2, "next_day_attempt_times": "10:00,14:00", - "call_timeout_seconds": 30, + "call_timeout_seconds": 30, "pickup_calls_enabled": False, } return dict(row) @@ -91,12 +91,13 @@ def get_groups_to_call(conn, settings): retry_min = settings.get("retry_interval_minutes", 120) max_attempts = settings.get("max_attempts", 5) no_input_escalate = settings.get("no_input_escalate_after", 2) + pickup_enabled = bool(settings.get("pickup_calls_enabled", False)) with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute(""" SELECT og.id, og.customer_phone, og.customer_phone_normalized, og.notification_status, og.delivery_status, og.sms_sent_at, - og.autocall_opt_out, og.created_at, + og.autocall_opt_out, og.created_at, og.delivery_type, (SELECT count(*) FROM autocall_log l WHERE l.order_group_id = og.id) as attempt_count, (SELECT count(*) FROM autocall_log l @@ -120,8 +121,10 @@ def get_groups_to_call(conn, settings): -- Не превышен лимит попыток AND (SELECT count(*) FROM autocall_log l WHERE l.order_group_id = og.id) < %s + -- Самовывоз звоним только если разрешено настройкой + AND (COALESCE(og.delivery_type, 'delivery') <> 'pickup' OR %s) ORDER BY og.sms_sent_at ASC - """, (delay_min, retry_min, max_attempts)) + """, (delay_min, retry_min, max_attempts, pickup_enabled)) return [dict(r) for r in cur.fetchall()] def is_work_time(settings, now=None): @@ -165,6 +168,19 @@ def get_exclusion_match(conn, phone): """, (phone, phone)) return cur.fetchone() +def resolve_call_type(group): + """Тип звонка: pkp (самовывоз) или dlv (доставка). + + Приоритет — явный call_type (ручной/тестовый запрос из очереди): его + выбирает логист. Если не задан — определяем по типу получения заказа. + От типа зависит озвучка (ru/ss_*_pkp vs ru/ss_*_dlv). + """ + ct = (group.get("call_type") or "").strip().lower() + if ct in ("dlv", "pkp"): + return ct + return "pkp" if (group.get("delivery_type") or "").strip().lower() == "pickup" else "dlv" + + def build_call_file(group, settings, attempt_no, is_test_call=False): """Формирует содержимое call-файла.""" phone = group.get("customer_phone_normalized") or group.get("customer_phone", "") @@ -184,7 +200,7 @@ def build_call_file(group, settings, attempt_no, is_test_call=False): # Сценарий: в день добавления группы → A (завтра/послезавтра), # со следующего дня → B (только завтра + половина дня). scenario = group.get("scenario") or resolve_scenario(group.get("created_at")) - call_type = "dlv" + call_type = resolve_call_type(group) lines = [ f"Channel: PJSIP/{dial}@mmedia", @@ -235,7 +251,8 @@ def get_queued_calls(conn): """Ручные запросы «Позвонить сейчас» из autocall_queue.""" with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute(""" - SELECT q.id as queue_id, q.order_group_id, q.phone, q.scenario, q.is_test_call + SELECT q.id as queue_id, q.order_group_id, q.phone, q.scenario, q.is_test_call, + q.call_type FROM autocall_queue q WHERE q.processed_at IS NULL ORDER BY q.requested_at ASC @@ -246,7 +263,8 @@ def get_queued_calls(conn): ids = tuple(r["order_group_id"] for r in rows if r["order_group_id"]) if ids: cur.execute(""" - SELECT og.id, og.customer_phone, og.customer_phone_normalized, og.created_at + SELECT og.id, og.customer_phone, og.customer_phone_normalized, + og.created_at, og.delivery_type FROM order_groups og WHERE og.id IN %s """, (ids,)) og = {r["id"]: r for r in cur.fetchall()} @@ -256,6 +274,7 @@ def get_queued_calls(conn): r["customer_phone_normalized"] = g.get("customer_phone_normalized") r.setdefault("customer_phone", g.get("customer_phone")) r.setdefault("created_at", g.get("created_at")) + r.setdefault("delivery_type", g.get("delivery_type")) return rows @@ -327,7 +346,7 @@ def backfill_no_answer(conn, settings): with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute(""" SELECT q.id, q.order_group_id, q.phone, q.is_test_call, - q.requested_at, q.processed_at + q.requested_at, q.processed_at, q.call_type FROM autocall_queue q WHERE q.processed_at IS NOT NULL AND q.processed_at < NOW() - make_interval(mins => 10) @@ -356,10 +375,11 @@ def backfill_no_answer(conn, settings): # Телефон из заказа (или нормализованный из заявки для pure-теста) phone = normalize_phone(q["phone"] or "") group_created = None + group_dtype = None if gid: with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute(""" - SELECT customer_phone_normalized, customer_phone, created_at + SELECT customer_phone_normalized, customer_phone, created_at, delivery_type FROM order_groups WHERE id = %s """, (gid,)) grow = cur.fetchone() or {} @@ -367,6 +387,7 @@ def backfill_no_answer(conn, settings): or normalize_phone(grow.get("customer_phone") or "") or phone) group_created = grow.get("created_at") + group_dtype = grow.get("delivery_type") # По исключённым номерам звонка не было — строку не пишем # (статус «исключение» и так показывается из настроек). if get_exclusion_match(conn, phone): @@ -379,14 +400,15 @@ def backfill_no_answer(conn, settings): else: attempt_no = 1 test_mode = bool(q["is_test_call"] or settings.get("test_mode", True)) + call_type = resolve_call_type({"call_type": q.get("call_type"), "delivery_type": group_dtype}) with conn.cursor() as cur: cur.execute(""" INSERT INTO autocall_log (order_group_id, customer_phone, attempt_no, scenario, - call_started_at, outcome, was_test_mode, channel_state) - VALUES (%s, %s, %s, %s, %s, 'no_answer', %s, 'NoAnswer') + call_started_at, outcome, was_test_mode, channel_state, call_type) + VALUES (%s, %s, %s, %s, %s, 'no_answer', %s, 'NoAnswer', %s) """, (gid, phone, attempt_no, scenario, - q["processed_at"] or q["requested_at"], test_mode)) + q["processed_at"] or q["requested_at"], test_mode, call_type)) conn.commit() backfilled += 1 log.info(f"Backfilled no_answer for queue item {q['id']} (phone={phone})") diff --git a/src/components/admin/autocall/AutocallChannelTab.jsx b/src/components/admin/autocall/AutocallChannelTab.jsx index f792a65..d0cdbea 100644 --- a/src/components/admin/autocall/AutocallChannelTab.jsx +++ b/src/components/admin/autocall/AutocallChannelTab.jsx @@ -285,6 +285,27 @@ export const AutocallChannelTab = () => { )} + {/* Самовывоз — отдельный выключатель: звонки клиентам, которые забирают со склада */} +