From f62a6b525b7c21c42d355dc7c22b90cd59e3b6ed Mon Sep 17 00:00:00 2001 From: Hermes Date: Fri, 11 Sep 2026 11:29:49 +0200 Subject: [PATCH] =?UTF-8?q?feat(autocall):=20=D0=BE=D0=BA=D0=BD=D0=BE=20?= =?UTF-8?q?=D1=81=D0=B0=D0=BC=D0=BE=D0=B2=D1=8B=D0=B2=D0=BE=D0=B7=D0=B0=20?= =?UTF-8?q?3=20=D0=B4=D0=BD=D1=8F=20(=D1=81=D0=B5=D0=B3=D0=BE=D0=B4=D0=BD?= =?UTF-8?q?=D1=8F/=D0=B7=D0=B0=D0=B2=D1=82=D1=80=D0=B0/=D0=BF=D0=BE=D1=81?= =?UTF-8?q?=D0=BB=D0=B5=D0=B7=D0=B0=D0=B2=D1=82=D1=80=D0=B0),=20=D0=B4?= =?UTF-8?q?=D0=B0=D1=82=D1=8B=20=D0=B8=D0=B7=20=D0=B4=D0=B8=D0=B0=D0=BB?= =?UTF-8?q?=D0=BE=D0=B3=D0=B0,=20=D0=B3=D0=B5=D0=BE-=D0=B8=D1=81=D0=BA?= =?UTF-8?q?=D0=BB=D1=8E=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BA=D0=B0=D0=BA?= =?UTF-8?q?=20=D1=83=20SMS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ats/ss-dialog.agi | 71 ++++++++++ .../sms_first_campaign.cpython-311.pyc | Bin 23996 -> 34414 bytes scripts/autocall_scheduler.py | 127 +++++++++++++++++- scripts/sms_first_campaign.py | 10 ++ scripts/sms_manual_campaign.py | 9 ++ scripts/sms_paid_storage_campaign.py | 9 ++ scripts/sms_second_campaign.py | 9 ++ src/components/client/PickupSlotsPicker.jsx | 84 +++++------- supabase/functions/autocall-result/index.ts | 32 ++++- 9 files changed, 295 insertions(+), 56 deletions(-) diff --git a/docs/ats/ss-dialog.agi b/docs/ats/ss-dialog.agi index ce81fdd..239d692 100644 --- a/docs/ats/ss-dialog.agi +++ b/docs/ats/ss-dialog.agi @@ -65,6 +65,12 @@ getvar GROUP_ID; GROUP_ID=$VAR getvar ATTEMPT; ATTEMPT=$VAR getvar TEST_MODE; TEST_MODE=$VAR getvar CUSTOMER_PHONE; CUSTOMER_PHONE=$VAR +# Даты самовывоза (заполняет планировщик по окну «готовность + 0..2 дня») +getvar PICKUP_DAY1; PICKUP_DAY1=$VAR +getvar PICKUP_DAY2; PICKUP_DAY2=$VAR +getvar PICKUP_DAY3; PICKUP_DAY3=$VAR +getvar PICKUP_DAY1_FIRST; PICKUP_DAY1_FIRST=$VAR +CHOSEN_DATE="" # Пишем состояние ПОСЛЕ КАЖДОГО шага: если клиент положит трубку, # выбор всё равно сохранён (AGI умирает молча, финальной записи не будет). @@ -105,6 +111,7 @@ send_result () { \"attempt_no\":${ATTEMPT:-1}, \"scenario\":\"$SCENARIO\", \"call_type\":\"${TYPE:-dlv}\", + \"chosen_date\":\"${CHOSEN_DATE:-}\", \"call_started_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\", \"answered_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\", \"duration_sec\":0, @@ -156,6 +163,70 @@ if [ "$SCENARIO" = "C" ]; then fi fi +# ─── Самовывоз: даты приходят из call-файла (PICKUP_DAY1..3) ───────── +# Робот называет конкретные дни, окно считает планировщик: +# готов сегодня → сегодня/завтра/послезавтра, вчера → сегодня/завтра, позавчера → только сегодня. +# Отвечаем выбранной датой (chosen_date), а не абстрактным «день 1/2». +if [ "$TYPE" = "pkp" ] && [ -n "$PICKUP_DAY1" ]; then + play "ru/ss_pkp_intro" + DAYS=1 + [ -n "$PICKUP_DAY2" ] && DAYS=2 + [ -n "$PICKUP_DAY3" ] && DAYS=3 + while [ -z "$OUTCOME" ]; do + digit "ru/ss_pkp_day${DAYS}" 9000 + CHOSEN_DAY=$DIGIT + case "$CHOSEN_DAY" in + 9) play "ru/ss_optout"; OUTCOME=opt_out; break ;; + 0) play "ru/ss_operator"; OUTCOME=operator; break ;; + 1|2|3) [ "$CHOSEN_DAY" -le "$DAYS" ] || CHOSEN_DAY="" ;; + *) CHOSEN_DAY="" ;; + esac + if [ -z "$CHOSEN_DAY" ]; then + digit "ru/ss_pkp_day${DAYS}" 9000 + CHOSEN_DAY=$DIGIT + case "$CHOSEN_DAY" in + 1|2|3) [ "$CHOSEN_DAY" -le "$DAYS" ] || CHOSEN_DAY="" ;; + *) CHOSEN_DAY="" ;; + esac + fi + if [ -z "$CHOSEN_DAY" ]; then + if [ -z "$ANY_DIGIT" ] && [ "$RING_SEC" -ge 20 ]; then + OUTCOME=no_answer; break + fi + play "ru/ss_no_input"; OUTCOME=no_input_day; break + fi + case "$CHOSEN_DAY" in + 1) CHOSEN_DATE="$PICKUP_DAY1" ;; + 2) CHOSEN_DATE="$PICKUP_DAY2" ;; + 3) CHOSEN_DATE="$PICKUP_DAY3" ;; + esac + DAY=$CHOSEN_DAY + save + # Половина дня: если осталось только «сегодня» и первая половина уже прошла — не спрашиваем. + if [ "$CHOSEN_DAY" = "1" ] && [ "$PICKUP_DAY1_FIRST" != "1" ]; then + HALF=2 + play "ru/ss_ok_second" + else + digit "ru/ss_half_pkp" 9000 + HALF=$DIGIT + if [ "$HALF" != "1" ] && [ "$HALF" != "2" ]; then + digit "ru/ss_half_pkp" 9000 + HALF=$DIGIT + fi + if [ "$HALF" != "1" ] && [ "$HALF" != "2" ]; then + if [ -z "$ANY_DIGIT" ] && [ "$RING_SEC" -ge 20 ]; then + OUTCOME=no_answer; break + fi + play "ru/ss_no_input"; OUTCOME=no_input_half; break + fi + [ "$HALF" = "1" ] && play "ru/ss_ok_first" || play "ru/ss_ok_second" + fi + CONF_DAY=$CHOSEN_DAY; CONF_HALF=$HALF + OUTCOME=confirmed + play "ru/ss_tail_pkp" + done +fi + # ─── выбор дня и времени, с возможностью переделать ────────────────── while [ -z "$OUTCOME" ]; do diff --git a/scripts/__pycache__/sms_first_campaign.cpython-311.pyc b/scripts/__pycache__/sms_first_campaign.cpython-311.pyc index 7950aa6f2f2bf62674bea09d5d7b6c7858d22e08..3752fafc9631933af7107036c07ab6cdbb452e9d 100644 GIT binary patch literal 34414 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 IF`J_Q7j>sRb^rhX diff --git a/scripts/autocall_scheduler.py b/scripts/autocall_scheduler.py index 794a803..83cf518 100644 --- a/scripts/autocall_scheduler.py +++ b/scripts/autocall_scheduler.py @@ -97,7 +97,7 @@ def get_groups_to_call(conn, settings): 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.delivery_type, + og.autocall_opt_out, og.created_at, og.delivery_type, og.town, (SELECT count(*) FROM autocall_log l WHERE l.order_group_id = og.id) as attempt_count, (SELECT count(*) FROM autocall_log l @@ -121,6 +121,20 @@ def get_groups_to_call(conn, settings): -- Не превышен лимит попыток AND (SELECT count(*) FROM autocall_log l WHERE l.order_group_id = og.id) < %s + -- Гео-исключения: те же, что у SMS (город исключён целиком + -- или канал для города выключен в профиле). + AND NOT EXISTS ( + SELECT 1 FROM sms_city_rules r + WHERE r.city = og.town + AND r.enabled = false + AND r.campaign_type IN ('*', 'autocall_slot') + AND (r.valid_from IS NULL OR r.valid_from <= CURRENT_DATE) + AND (r.valid_until IS NULL OR r.valid_until >= CURRENT_DATE) + ) + AND NOT EXISTS ( + SELECT 1 FROM sms_city_profiles p + WHERE p.city = og.town AND p.sms_enabled = false + ) -- Самовывоз звоним только если разрешено настройкой AND (COALESCE(og.delivery_type, 'delivery') <> 'pickup' OR %s) ORDER BY og.sms_sent_at ASC @@ -151,6 +165,46 @@ def normalize_phone(phone): clean = "7" + clean return clean +def get_city_exclusion(conn, city): + """Гео-исключение — общее для SMS и звонков. + + Источник тот же, что у SMS: правила города (sms_city_rules) и общий + выключатель канала в профиле города (sms_city_profiles.sms_enabled). + Правило с campaign_type='*' действует на все каналы, 'autocall_slot' — + только на звонки. + + Исключения по номеру (например, «не звонить на МТС», autocall_exclusions) + живут отдельно и на SMS не влияют — они про телефонию, а не про город. + """ + if not city: + return None + with conn.cursor(cursor_factory=RealDictCursor) as cur: + cur.execute(""" + SELECT city, campaign_type, note + FROM sms_city_rules + WHERE city = %s + AND enabled = false + AND campaign_type IN ('*', 'autocall_slot') + AND (valid_from IS NULL OR valid_from <= CURRENT_DATE) + AND (valid_until IS NULL OR valid_until >= CURRENT_DATE) + LIMIT 1 + """, (city,)) + rule = cur.fetchone() + if rule: + return { + "source": "city_rule", + "reason": (rule.get("note") or "").strip() or f"город {city} исключён из рассылок", + } + cur.execute("SELECT sms_enabled, note FROM sms_city_profiles WHERE city = %s LIMIT 1", (city,)) + profile = cur.fetchone() + if profile is not None and profile.get("sms_enabled") is False: + return { + "source": "city_profile", + "reason": (profile.get("note") or "").strip() or f"город {city}: канал выключен", + } + return None + + def get_exclusion_match(conn, phone): """Возвращает правило исключения, если на этот номер звонить нельзя, иначе None.""" phone = normalize_phone(phone) @@ -168,6 +222,40 @@ def get_exclusion_match(conn, phone): """, (phone, phone)) return cur.fetchone() +def pickup_window(sms_sent_at, now=None): + """Даты, на которые предлагаем самовывоз. + + Правило: забрать можно в день готовности (день попадания в базу — он же + день отправки ссылки) и два следующих дня, то есть максимум три дня + включая сегодняшний. Окно отсчитывается от дня готовности, а список + начинается с сегодня: + • готов сегодня → сегодня, завтра, послезавтра (3) + • готов вчера → сегодня, завтра (2) + • готов позавчера → только сегодня (1) + • готов раньше → окно закрыто, звонить не о чем (0) + """ + now = now or datetime.now(timezone.utc) + tz = timezone(timedelta(hours=3)) + today = now.astimezone(tz).date() + ready = (sms_sent_at or now).astimezone(tz).date() + last_allowed = ready + timedelta(days=2) + if last_allowed < today: + return [] + dates = [] + day = today + while day <= last_allowed and len(dates) < 3: + dates.append(day) + day += timedelta(days=1) + return [d.isoformat() for d in dates] + + +def is_today_before_noon(now=None): + """До 12:00 по Симферополю — первую половину дня ещё можно предлагать.""" + now = now or datetime.now(timezone.utc) + local = now.astimezone(timezone(timedelta(hours=3))) + return local.hour < 12 + + def resolve_call_type(group): """Тип звонка: pkp (самовывоз) или dlv (доставка). @@ -202,6 +290,15 @@ def build_call_file(group, settings, attempt_no, is_test_call=False): scenario = group.get("scenario") or resolve_scenario(group.get("created_at")) call_type = resolve_call_type(group) + # Самовывоз: конкретные даты передаём в диалог явно — робот называет их + # голосом и возвращает выбранную дату, а не абстрактный «день 1/2». + pickup_days = [] + if call_type == "pkp": + pickup_days = pickup_window(group.get("sms_sent_at") or group.get("created_at")) + if not pickup_days: + # Окно самовывоза закрыто (заказ готов более двух дней назад). + return None + lines = [ f"Channel: PJSIP/{dial}@mmedia", f"CallerID: {CALLER_ID}", @@ -216,6 +313,10 @@ def build_call_file(group, settings, attempt_no, is_test_call=False): f"Setvar: GROUP_ID={group.get('id') or ''}", f"Setvar: ATTEMPT={attempt_no}", f"Setvar: CUSTOMER_PHONE={phone}", + f"Setvar: PICKUP_DAY1={pickup_days[0] if len(pickup_days) > 0 else ''}", + f"Setvar: PICKUP_DAY2={pickup_days[1] if len(pickup_days) > 1 else ''}", + f"Setvar: PICKUP_DAY3={pickup_days[2] if len(pickup_days) > 2 else ''}", + f"Setvar: PICKUP_DAY1_FIRST={1 if (pickup_days and is_today_before_noon()) else 0}", f"Setvar: API_URL={API_URL}", f"Setvar: API_KEY={API_KEY}", f"Setvar: TEST_MODE={1 if test_mode else 0}", @@ -226,6 +327,10 @@ def build_call_file(group, settings, attempt_no, is_test_call=False): def push_call_file(group, settings, attempt_no, is_test_call=False): """Кладёт call-файл на АТС через SSH.""" content = build_call_file(group, settings, attempt_no, is_test_call) + if content is None: + # Звонить не о чем (например, окно самовывоза закрыто) — тихо пропускаем. + log.info(f"Skip group {group.get('id')}: нет доступных вариантов для звонка") + return False gid = str(group.get("id") or "test")[:8] filename = f"ss-{gid}-{int(time.time())}.call" # Пишем файл на АТС через sshpass + cat @@ -264,7 +369,7 @@ def get_queued_calls(conn): if ids: cur.execute(""" SELECT og.id, og.customer_phone, og.customer_phone_normalized, - og.created_at, og.delivery_type + og.created_at, og.delivery_type, og.town FROM order_groups og WHERE og.id IN %s """, (ids,)) og = {r["id"]: r for r in cur.fetchall()} @@ -275,6 +380,7 @@ def get_queued_calls(conn): r.setdefault("customer_phone", g.get("customer_phone")) r.setdefault("created_at", g.get("created_at")) r.setdefault("delivery_type", g.get("delivery_type")) + r.setdefault("town", g.get("town")) return rows @@ -300,6 +406,12 @@ def process_queue(conn, settings): if not phone: log.warning(f"Queue item {q['queue_id']} has no phone, skip") continue + # Гео-исключение (как для SMS): город исключён целиком + city_excl = get_city_exclusion(conn, q.get("town")) + if city_excl: + log.info(f"Queue item {q['queue_id']} skipped by city rule ({q.get('town')}): {city_excl['reason']}") + mark_queue_processed(conn, q["queue_id"]) + continue # Исключение: на этот номер не звоним (например, МТС-диапазон) excl = get_exclusion_match(conn, phone) if excl: @@ -379,7 +491,8 @@ def backfill_no_answer(conn, settings): if gid: with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute(""" - SELECT customer_phone_normalized, customer_phone, created_at, delivery_type + SELECT customer_phone_normalized, customer_phone, created_at, + delivery_type, town FROM order_groups WHERE id = %s """, (gid,)) grow = cur.fetchone() or {} @@ -392,6 +505,9 @@ def backfill_no_answer(conn, settings): # (статус «исключение» и так показывается из настроек). if get_exclusion_match(conn, phone): continue + # Гео-исключение: город, по которому звонки не делаем вовсе + if get_city_exclusion(conn, q.get("town")): + continue scenario = resolve_scenario(group_created) if gid else "A" with conn.cursor() as cur: if gid: @@ -453,6 +569,11 @@ def main(): pushed = 0 for group in groups: phone = group.get("customer_phone_normalized") or group.get("customer_phone") or "" + # Гео-исключение (то же, что для SMS): город исключён целиком + city_excl = get_city_exclusion(conn, group.get("town")) + if city_excl: + log.info(f"Group {group['id']} skipped by city rule ({group.get('town')}): {city_excl['reason']}") + continue # Исключение: на этот номер не звоним (например, МТС-диапазон) excl = get_exclusion_match(conn, phone) if excl: diff --git a/scripts/sms_first_campaign.py b/scripts/sms_first_campaign.py index 13cd80f..db22bd9 100644 --- a/scripts/sms_first_campaign.py +++ b/scripts/sms_first_campaign.py @@ -210,6 +210,16 @@ def get_groups_to_send(conn): AND COALESCE(og.notification_status, '') = 'link_ready' -- Профиль sms_enabled (замена старого sms_allowed_for_city) AND COALESCE(scp.sms_enabled, true) = true + -- Гео-исключения — общие для SMS и звонков: правило города + -- (campaign_type '*' — все каналы) блокирует и отправку SMS. + AND NOT EXISTS ( + SELECT 1 FROM sms_city_rules r + WHERE r.city = og.town + AND r.enabled = false + AND r.campaign_type IN ('*', 'first_sms') + AND (r.valid_from IS NULL OR r.valid_from <= CURRENT_DATE) + AND (r.valid_until IS NULL OR r.valid_until >= CURRENT_DATE) + ) -- Нет активной SMS в логе (sent/checking) за последние 2 часа AND NOT EXISTS ( SELECT 1 FROM sms_campaign_log scl diff --git a/scripts/sms_manual_campaign.py b/scripts/sms_manual_campaign.py index 6a30f97..53e46b5 100644 --- a/scripts/sms_manual_campaign.py +++ b/scripts/sms_manual_campaign.py @@ -112,6 +112,15 @@ def get_groups_to_manual(conn): og.sms_attempts FROM order_groups og WHERE og.delivery_status = 'pending_confirmation' + -- Гео-исключения — общие для SMS и звонков + AND NOT EXISTS ( + SELECT 1 FROM sms_city_rules r + WHERE r.city = og.town + AND r.enabled = false + AND r.campaign_type IN ('*', 'manual') + AND (r.valid_from IS NULL OR r.valid_from <= CURRENT_DATE) + AND (r.valid_until IS NULL OR r.valid_until >= CURRENT_DATE) + ) AND ( -- 1. second_sms_sent + срок истёк (COALESCE(og.notification_status, '') = 'second_sms_sent' diff --git a/scripts/sms_paid_storage_campaign.py b/scripts/sms_paid_storage_campaign.py index d15e15b..375de31 100644 --- a/scripts/sms_paid_storage_campaign.py +++ b/scripts/sms_paid_storage_campaign.py @@ -183,6 +183,15 @@ def get_groups_to_send(conn): LEFT JOIN sms_city_profiles scp ON scp.city = og.town WHERE og.delivery_status = 'paid_storage' AND og.paid_storage_at IS NOT NULL + -- Гео-исключения — общие для SMS и звонков + AND NOT EXISTS ( + SELECT 1 FROM sms_city_rules r + WHERE r.city = og.town + AND r.enabled = false + AND r.campaign_type IN ('*', 'paid_storage') + AND (r.valid_from IS NULL OR r.valid_from <= CURRENT_DATE) + AND (r.valid_until IS NULL OR r.valid_until >= CURRENT_DATE) + ) AND COALESCE(og.notification_status, '') NOT IN ('paid_storage_sending', 'paid_storage_sent') AND og.delivery_link IS NOT NULL AND og.delivery_link != '' diff --git a/scripts/sms_second_campaign.py b/scripts/sms_second_campaign.py index da4bde4..d8f605f 100644 --- a/scripts/sms_second_campaign.py +++ b/scripts/sms_second_campaign.py @@ -209,6 +209,15 @@ def get_groups_to_send(conn): AND (og.next_notification_check_at IS NULL OR og.next_notification_check_at <= NOW()) -- Профиль: second_sms_enabled (если false — 2-я SMS не отправляется) AND COALESCE(scp.second_sms_enabled, true) = true + -- Гео-исключения — общие для SMS и звонков + AND NOT EXISTS ( + SELECT 1 FROM sms_city_rules r + WHERE r.city = og.town + AND r.enabled = false + AND r.campaign_type IN ('*', 'second_sms') + AND (r.valid_from IS NULL OR r.valid_from <= CURRENT_DATE) + AND (r.valid_until IS NULL OR r.valid_until >= CURRENT_DATE) + ) -- Нет активной второй SMS в логе AND NOT EXISTS ( SELECT 1 FROM sms_campaign_log scl diff --git a/src/components/client/PickupSlotsPicker.jsx b/src/components/client/PickupSlotsPicker.jsx index c5976b6..48ce41c 100644 --- a/src/components/client/PickupSlotsPicker.jsx +++ b/src/components/client/PickupSlotsPicker.jsx @@ -53,65 +53,55 @@ const getNextPickupWorkday = (dateKey, allowedDays = [1,2,3,4,5]) => { return next; }; -const getPickupSlots = (referenceDate = new Date(), pickupDays = [1,2,3,4,5]) => { - const todayKey = getCrimeaTodayKey(referenceDate); - const hour = getCrimeaHour(referenceDate); - const isTodayWorkday = isAllowedPickupDay(todayKey, pickupDays); +/** + * Слоты самовывоза. + * + * Окно считаем от дня готовности (referenceDate — день попадания в базу, он же + * день отправки ссылки): забрать можно в день готовности и два следующих дня, + * максимум три дня включая сегодняшний. Отсчёт в списке — от фактического + * сегодня: если заказ готов вчера, останутся только «Сегодня» и «Завтра». + */ +const getPickupSlots = (referenceDate = new Date(), pickupDays = [1,2,3,4,5], now = new Date()) => { + const todayKey = getCrimeaTodayKey(now); + const hour = getCrimeaHour(now); + const readyKey = getCrimeaTodayKey(referenceDate); + const lastAllowedKey = addDaysKey(readyKey, 2); + // Дата попадает в окно: не раньше сегодня и не позже «готовность + 2 дня». + const isInWindow = (dateKey) => Boolean(dateKey) && dateKey >= todayKey && dateKey <= lastAllowedKey; const slots = []; - if (isTodayWorkday) { - if (hour < 12) { - slots.push({ - id: `pickup-${todayKey}-first`, - date: todayKey, - time: "Первая половина дня", - label: "Сегодня", - pickupType: "today", - }); - } + const addSlot = (dateKey, half, label, pickupType) => { slots.push({ - id: `pickup-${todayKey}-second`, - date: todayKey, - time: "Вторая половина дня", - label: "Сегодня", - pickupType: "today", + id: `pickup-${dateKey}-${half}`, + date: dateKey, + time: half === "first" ? "Первая половина дня" : "Вторая половина дня", + label, + pickupType, }); + }; + + // Сегодня: первая половина — только до 12:00, вторая — в течение дня. + if (isAllowedPickupDay(todayKey, pickupDays) && isInWindow(todayKey)) { + if (hour < 12) addSlot(todayKey, "first", "Сегодня", "today"); + addSlot(todayKey, "second", "Сегодня", "today"); } const tomorrow = addDaysKey(todayKey, 1); const tomorrowWorkday = isAllowedPickupDay(tomorrow, pickupDays) ? tomorrow : getNextPickupWorkday(todayKey, pickupDays); - slots.push({ - id: `pickup-${tomorrowWorkday}-first`, - date: tomorrowWorkday, - time: "Первая половина дня", - label: getDeliveryRelativeDayLabel(tomorrowWorkday, referenceDate) || "Завтра", - pickupType: "tomorrow", - }); - slots.push({ - id: `pickup-${tomorrowWorkday}-second`, - date: tomorrowWorkday, - time: "Вторая половина дня", - label: getDeliveryRelativeDayLabel(tomorrowWorkday, referenceDate) || "Завтра", - pickupType: "tomorrow", - }); + if (isInWindow(tomorrowWorkday)) { + const label = getDeliveryRelativeDayLabel(tomorrowWorkday, now) || "Завтра"; + addSlot(tomorrowWorkday, "first", label, "tomorrow"); + addSlot(tomorrowWorkday, "second", label, "tomorrow"); + } const dayAfter = addDaysKey(tomorrowWorkday, 1); const dayAfterWorkday = isAllowedPickupDay(dayAfter, pickupDays) ? dayAfter : getNextPickupWorkday(dayAfter, pickupDays); - slots.push({ - id: `pickup-${dayAfterWorkday}-first`, - date: dayAfterWorkday, - time: "Первая половина дня", - label: getDeliveryRelativeDayLabel(dayAfterWorkday, referenceDate) || "Послезавтра", - pickupType: "dayAfter", - }); - slots.push({ - id: `pickup-${dayAfterWorkday}-second`, - date: dayAfterWorkday, - time: "Вторая половина дня", - label: getDeliveryRelativeDayLabel(dayAfterWorkday, referenceDate) || "Послезавтра", - pickupType: "dayAfter", - }); + if (isInWindow(dayAfterWorkday)) { + const label = getDeliveryRelativeDayLabel(dayAfterWorkday, now) || "Послезавтра"; + addSlot(dayAfterWorkday, "first", label, "dayAfter"); + addSlot(dayAfterWorkday, "second", label, "dayAfter"); + } return slots; }; diff --git a/supabase/functions/autocall-result/index.ts b/supabase/functions/autocall-result/index.ts index 438070c..64829d4 100644 --- a/supabase/functions/autocall-result/index.ts +++ b/supabase/functions/autocall-result/index.ts @@ -30,6 +30,7 @@ type AutocallBody = { attempt_no?: number; scenario?: string; call_type?: string; + chosen_date?: string; call_started_at?: string; answered_at?: string | null; duration_sec?: number | null; @@ -51,6 +52,16 @@ function addDays(date: Date, days: number): string { return d.toISOString().slice(0, 10); } +/** День по-человечески: сегодня / завтра / послезавтра, иначе дата дд.мм.гггг. */ +function relativeDayLabel(dateKey: string): string { + const today = new Date(); + for (const [offset, label] of [[0, "сегодня"], [1, "завтра"], [2, "послезавтра"]] as const) { + if (addDays(today, offset) === dateKey) return label; + } + const [y, m, d] = dateKey.split("-"); + return y && m && d ? `${d}.${m}.${y}` : dateKey; +} + Deno.serve(async (request) => { if (request.method === "OPTIONS") { const cors = getCorsHeaders(request, "private"); @@ -151,27 +162,36 @@ Deno.serve(async (request) => { // ── confirmed*: записываем слот ── if (["confirmed", "confirmed_kept_previous", "confirmed_silent"].includes(body.outcome)) { - const dayOffset = body.dtmf_day === 2 ? 2 : 1; - const deliveryDate = addDays(new Date(), dayOffset); + // День называет сам диалог (chosen_date) — робот перечисляет конкретные + // дни окна самовывоза, поэтому «1/2/3» само по себе дату не задаёт. + // Если дата не пришла (старые call-файлы) — считаем по цифре дня. + const chosenDate = /^\d{4}-\d{2}-\d{2}$/.test(body.chosen_date || "") + ? (body.chosen_date as string) + : null; + const dayOffset = body.dtmf_day === 3 ? 2 : body.dtmf_day === 2 ? 1 : 0; + const deliveryDate = chosenDate || addDays(new Date(), body.dtmf_day ? dayOffset : 1); const timeSlot = body.dtmf_half ? HALF_LABEL[body.dtmf_half] : null; - const dayLabel = body.dtmf_day ? DAY_LABEL[body.dtmf_day] : "завтра"; + const dayLabel = chosenDate + ? relativeDayLabel(chosenDate) + : body.dtmf_day + ? DAY_LABEL[body.dtmf_day] + : "завтра"; const comment = `Согласовано роботом: ${dayLabel}${timeSlot ? ", " + timeSlot.toLowerCase() : ""}`; const update: Record = { delivery_date_source: "autocall", delivery_status: "agreed", notification_status: "confirmed", + delivery_date: deliveryDate, called: true, called_at: new Date().toISOString(), called_comment: comment, next_notification_check_at: null, }; + if (timeSlot) update.delivery_time = timeSlot; if (isPickup) { update.pickup_date = deliveryDate; if (timeSlot) update.pickup_time_slot = timeSlot; - } else { - update.delivery_date = deliveryDate; - if (timeSlot) update.delivery_time = timeSlot; } await supabase.from("order_groups").update(update).eq("id", orderGroupId); }