fix(autocall): самовывоз только по рабочим дням графика + робот называет реальные дни; страница клиента читает график
This commit is contained in:
parent
139c895290
commit
270ab39abc
|
|
@ -30,6 +30,16 @@ digit () { # $1=озвучка, $2=таймаут мс -> DIGIT
|
|||
DIGIT=$(echo "$RESP" | sed -n 's/.*result=\([0-9]*\).*/\1/p')
|
||||
[ -n "$DIGIT" ] && ANY_DIGIT=1
|
||||
}
|
||||
|
||||
wait_digit () { # $1=таймаут мс -> DIGIT (после EXEC Playback; 0 у Asterisk = тишина)
|
||||
say "WAIT FOR DIGIT ${1:-9000}"
|
||||
DIGIT=$(echo "$RESP" | sed -n 's/.*result=\([0-9]*\).*/\1/p')
|
||||
if [ "$DIGIT" = "0" ] || [ -z "$DIGIT" ]; then
|
||||
DIGIT=""
|
||||
else
|
||||
ANY_DIGIT=1
|
||||
fi
|
||||
}
|
||||
# Чтение channel-переменной из call-файла (Setvar) через AGI.
|
||||
getvar () { # $1=имя -> VAR
|
||||
say "GET VARIABLE $1"
|
||||
|
|
@ -172,17 +182,57 @@ if [ "$TYPE" = "pkp" ] && [ -n "$PICKUP_DAY1" ]; then
|
|||
DAYS=1
|
||||
[ -n "$PICKUP_DAY2" ] && DAYS=2
|
||||
[ -n "$PICKUP_DAY3" ] && DAYS=3
|
||||
|
||||
# Дни идут подряд от сегодня → играем готовые подсказки (живая фраза целиком).
|
||||
# Если внутри окна выходной (например, пятница → понедельник), подсказка
|
||||
# «послезавтра» соврала бы — собираем меню из фрагментов и называем реальные дни.
|
||||
PICK_TODAY=$(date +%F)
|
||||
PICK_TOMORROW=$(date -d "+1 day" +%F)
|
||||
PICK_DAYAFTER=$(date -d "+2 days" +%F)
|
||||
CONTIG=1
|
||||
# Первый вариант — всегда «сегодня»; если окно начинается не с сегодня
|
||||
# (звонок попал на выходной), подсказка «сегодня» соврала бы.
|
||||
if [ "$PICKUP_DAY1" != "$PICK_TODAY" ]; then CONTIG=0; fi
|
||||
if [ "$DAYS" -ge 2 ] && [ "$PICKUP_DAY2" != "$PICK_TOMORROW" ]; then CONTIG=0; fi
|
||||
if [ "$DAYS" -ge 3 ] && [ "$PICKUP_DAY3" != "$PICK_DAYAFTER" ]; then CONTIG=0; fi
|
||||
|
||||
if [ "$CONTIG" != "1" ]; then
|
||||
day_frag () {
|
||||
case "$1" in
|
||||
"$PICK_TODAY") echo "ru/ss_pkp_d_today" ;;
|
||||
"$PICK_TOMORROW") echo "ru/ss_pkp_d_tomorrow" ;;
|
||||
*) echo "ru/ss_pkp_d_$(date -d "$1" +%u)" ;;
|
||||
esac
|
||||
}
|
||||
if [ "$DAYS" = "1" ]; then
|
||||
PICK_MENU="ru/ss_pkp_only&$(day_frag "$PICKUP_DAY1")"
|
||||
else
|
||||
PICK_MENU="ru/ss_pkp_menu1&$(day_frag "$PICKUP_DAY1")"
|
||||
[ -n "$PICKUP_DAY2" ] && PICK_MENU="${PICK_MENU}&ru/ss_pkp_menu2&$(day_frag "$PICKUP_DAY2")"
|
||||
[ -n "$PICKUP_DAY3" ] && PICK_MENU="${PICK_MENU}&ru/ss_pkp_menu3&$(day_frag "$PICKUP_DAY3")"
|
||||
fi
|
||||
fi
|
||||
|
||||
while [ -z "$OUTCOME" ]; do
|
||||
digit "ru/ss_pkp_day${DAYS}" 9000
|
||||
if [ "$CONTIG" = "1" ]; then
|
||||
digit "ru/ss_pkp_day${DAYS}" 9000
|
||||
else
|
||||
say "EXEC Playback \"$PICK_MENU\""
|
||||
wait_digit 9000
|
||||
fi
|
||||
CHOSEN_DAY=$DIGIT
|
||||
case "$CHOSEN_DAY" in
|
||||
9) play "ru/ss_optout"; OUTCOME=opt_out; break ;;
|
||||
0) play "ru/ss_operator"; OUTCOME=operator; break ;;
|
||||
9) play "ru/ss_optout"; OUTCOME=opt_out; 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
|
||||
if [ "$CONTIG" = "1" ]; then
|
||||
digit "ru/ss_pkp_day${DAYS}" 9000
|
||||
else
|
||||
say "EXEC Playback \"$PICK_MENU\""
|
||||
wait_digit 9000
|
||||
fi
|
||||
CHOSEN_DAY=$DIGIT
|
||||
case "$CHOSEN_DAY" in
|
||||
1|2|3) [ "$CHOSEN_DAY" -le "$DAYS" ] || CHOSEN_DAY="" ;;
|
||||
|
|
|
|||
|
|
@ -236,19 +236,41 @@ def get_exclusion_match(conn, phone):
|
|||
""", (phone, phone))
|
||||
return cur.fetchone()
|
||||
|
||||
def pickup_window(sms_sent_at, now=None):
|
||||
def load_work_days(conn, field="pickup_days"):
|
||||
"""Рабочие дни по графику компании (1=пн … 7=вс).
|
||||
|
||||
График правит админ в «График работы» (business_schedule_settings).
|
||||
Звонки обязаны предлагать только рабочие дни: в пятницу «послезавтра» —
|
||||
это воскресенье, когда самовывоз не работает.
|
||||
"""
|
||||
default = [1, 2, 3, 4, 5]
|
||||
try:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute("SELECT pickup_days FROM business_schedule_settings WHERE id = 1")
|
||||
row = cur.fetchone() or {}
|
||||
raw = row.get("pickup_days") or default
|
||||
days = sorted({int(str(d).strip()) for d in raw if str(d).strip().isdigit()})
|
||||
return days or default
|
||||
except Exception as e:
|
||||
log.warning(f"Не удалось прочитать график работы, беру пн-пт: {e}")
|
||||
return default
|
||||
|
||||
|
||||
def pickup_window(sms_sent_at, work_days=None, now=None):
|
||||
"""Даты, на которые предлагаем самовывоз.
|
||||
|
||||
Правило: забрать можно в день готовности (день попадания в базу — он же
|
||||
день отправки ссылки) и два следующих дня, то есть максимум три дня
|
||||
включая сегодняшний. Окно отсчитывается от дня готовности, а список
|
||||
начинается с сегодня:
|
||||
• готов сегодня → сегодня, завтра, послезавтра (3)
|
||||
включая сегодняшний. Выходные и нерабочие дни пропускаем, отсчёт ведём
|
||||
от сегодня:
|
||||
• готов сегодня → сегодня, завтра, послезавтра (3 рабочих дня)
|
||||
• готов вчера → сегодня, завтра (2)
|
||||
• готов позавчера → только сегодня (1)
|
||||
• готов позавчера → сегодня (1)
|
||||
• готов раньше → окно закрыто, звонить не о чем (0)
|
||||
• в пятницу готов → пятница и суббота (воскресенье закрыто)
|
||||
"""
|
||||
now = now or datetime.now(timezone.utc)
|
||||
work_days = work_days or [1, 2, 3, 4, 5]
|
||||
tz = timezone(timedelta(hours=3))
|
||||
today = now.astimezone(tz).date()
|
||||
ready = (sms_sent_at or now).astimezone(tz).date()
|
||||
|
|
@ -258,7 +280,8 @@ def pickup_window(sms_sent_at, now=None):
|
|||
dates = []
|
||||
day = today
|
||||
while day <= last_allowed and len(dates) < 3:
|
||||
dates.append(day)
|
||||
if day.isoweekday() in work_days:
|
||||
dates.append(day)
|
||||
day += timedelta(days=1)
|
||||
return [d.isoformat() for d in dates]
|
||||
|
||||
|
|
@ -308,7 +331,10 @@ def build_call_file(group, settings, attempt_no, is_test_call=False):
|
|||
# голосом и возвращает выбранную дату, а не абстрактный «день 1/2».
|
||||
pickup_days = []
|
||||
if call_type == "pkp":
|
||||
pickup_days = pickup_window(group.get("sms_sent_at") or group.get("created_at"))
|
||||
pickup_days = pickup_window(
|
||||
group.get("sms_sent_at") or group.get("created_at"),
|
||||
settings.get("pickup_work_days"),
|
||||
)
|
||||
if not pickup_days:
|
||||
# Окно самовывоза закрыто (заказ готов более двух дней назад).
|
||||
return None
|
||||
|
|
@ -549,6 +575,8 @@ def main():
|
|||
conn = get_db_conn()
|
||||
try:
|
||||
settings = load_settings(conn)
|
||||
# Рабочие дни для окна самовывоза (выходные робот не предлагает)
|
||||
settings["pickup_work_days"] = load_work_days(conn)
|
||||
|
||||
# Добор «не взял трубку»: заявки, по которым сеть так и не ответила
|
||||
# (истёкший call-файл, AGI не запускался). Только работа с БД.
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { formatDeliveryDate } from "../components/client/deliveryDateFormatting"
|
|||
import {
|
||||
confirmDeliveryChoice,
|
||||
fetchDeliveryInvitation,
|
||||
fetchPickupWorkDays,
|
||||
} from "../services/deliveryInvitationApi";
|
||||
|
||||
const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL || "";
|
||||
|
|
@ -305,6 +306,20 @@ export const ClientDeliveryPage = () => {
|
|||
[token, invitation?.smsSentAt],
|
||||
);
|
||||
|
||||
// Рабочие дни самовывоза — из графика компании, те же, что называет робот.
|
||||
const [pickupWorkDays, setPickupWorkDays] = React.useState([1, 2, 3, 4, 5]);
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
fetchPickupWorkDays()
|
||||
.then((days) => {
|
||||
if (!cancelled) setPickupWorkDays(days);
|
||||
})
|
||||
.catch(() => {});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
|
|
@ -642,6 +657,7 @@ export const ClientDeliveryPage = () => {
|
|||
onSelectSlot={handleSlotSelect}
|
||||
selectedSlotId={selectedSlotId}
|
||||
referenceDate={referenceDate}
|
||||
pickupDays={pickupWorkDays}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
|
|
|
|||
|
|
@ -183,6 +183,31 @@ export const __resetLocalDeliveryInvitationCache = () => {
|
|||
localDeliveryInvitationCache.clear();
|
||||
};
|
||||
|
||||
/**
|
||||
* Рабочие дни самовывоза из графика компании («График работы»).
|
||||
* Робот и страница клиента обязаны предлагать одни и те же дни, иначе
|
||||
* клиент выберет день, который склад не работает (например, воскресенье).
|
||||
* Номера дней: 1=пн … 7=вс.
|
||||
*/
|
||||
export const fetchPickupWorkDays = async () => {
|
||||
const fallback = [1, 2, 3, 4, 5];
|
||||
if (!hasSupabaseConfig || !supabase?.rpc) {
|
||||
return fallback;
|
||||
}
|
||||
try {
|
||||
const { data, error } = await supabase.rpc("get_business_schedule");
|
||||
if (error) throw error;
|
||||
const days = Array.isArray(data?.pickupDays) ? data.pickupDays : [];
|
||||
const parsed = days
|
||||
.map((d) => Number(d))
|
||||
.filter((d) => Number.isInteger(d) && d >= 1 && d <= 7);
|
||||
return parsed.length ? parsed : fallback;
|
||||
} catch (e) {
|
||||
console.warn("Не удалось получить график самовывоза, беру пн-пт", e);
|
||||
return fallback;
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchDeliveryInvitation = async (token) => {
|
||||
if (!token) {
|
||||
throw new Error("Token is required");
|
||||
|
|
|
|||
Loading…
Reference in New Issue