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 7950aa6..3752faf 100644 Binary files a/scripts/__pycache__/sms_first_campaign.cpython-311.pyc and b/scripts/__pycache__/sms_first_campaign.cpython-311.pyc differ 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); }