feat(autocall): окно самовывоза 3 дня (сегодня/завтра/послезавтра), даты из диалога, гео-исключения как у SMS
This commit is contained in:
parent
feb3fc1b07
commit
f62a6b525b
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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 != ''
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
const addSlot = (dateKey, half, label, pickupType) => {
|
||||
slots.push({
|
||||
id: `pickup-${todayKey}-first`,
|
||||
date: todayKey,
|
||||
time: "Первая половина дня",
|
||||
label: "Сегодня",
|
||||
pickupType: "today",
|
||||
});
|
||||
}
|
||||
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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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<string, unknown> = {
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue