feat(autocall): самовывоз — тип звонка pkp/dlv из карточки заказа, выключатель самовывоза, тип в логе и очереди
This commit is contained in:
parent
46a3609233
commit
c4e2c4a181
|
|
@ -84,6 +84,7 @@ send_result () {
|
||||||
\"customer_phone\":\"${CUSTOMER_PHONE:-}\",
|
\"customer_phone\":\"${CUSTOMER_PHONE:-}\",
|
||||||
\"attempt_no\":${ATTEMPT:-1},
|
\"attempt_no\":${ATTEMPT:-1},
|
||||||
\"scenario\":\"$SCENARIO\",
|
\"scenario\":\"$SCENARIO\",
|
||||||
|
\"call_type\":\"${TYPE:-dlv}\",
|
||||||
\"call_started_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",
|
\"call_started_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",
|
||||||
\"answered_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",
|
\"answered_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",
|
||||||
\"duration_sec\":0,
|
\"duration_sec\":0,
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ def load_settings(conn):
|
||||||
"delay_after_sms_minutes": 90, "retry_interval_minutes": 120,
|
"delay_after_sms_minutes": 90, "retry_interval_minutes": 120,
|
||||||
"attempts_per_day": 3, "max_attempts": 5,
|
"attempts_per_day": 3, "max_attempts": 5,
|
||||||
"no_input_escalate_after": 2, "next_day_attempt_times": "10:00,14:00",
|
"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)
|
return dict(row)
|
||||||
|
|
||||||
|
|
@ -91,12 +91,13 @@ def get_groups_to_call(conn, settings):
|
||||||
retry_min = settings.get("retry_interval_minutes", 120)
|
retry_min = settings.get("retry_interval_minutes", 120)
|
||||||
max_attempts = settings.get("max_attempts", 5)
|
max_attempts = settings.get("max_attempts", 5)
|
||||||
no_input_escalate = settings.get("no_input_escalate_after", 2)
|
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:
|
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT og.id, og.customer_phone, og.customer_phone_normalized,
|
SELECT og.id, og.customer_phone, og.customer_phone_normalized,
|
||||||
og.notification_status, og.delivery_status, og.sms_sent_at,
|
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
|
(SELECT count(*) FROM autocall_log l
|
||||||
WHERE l.order_group_id = og.id) as attempt_count,
|
WHERE l.order_group_id = og.id) as attempt_count,
|
||||||
(SELECT count(*) FROM autocall_log l
|
(SELECT count(*) FROM autocall_log l
|
||||||
|
|
@ -120,8 +121,10 @@ def get_groups_to_call(conn, settings):
|
||||||
-- Не превышен лимит попыток
|
-- Не превышен лимит попыток
|
||||||
AND (SELECT count(*) FROM autocall_log l
|
AND (SELECT count(*) FROM autocall_log l
|
||||||
WHERE l.order_group_id = og.id) < %s
|
WHERE l.order_group_id = og.id) < %s
|
||||||
|
-- Самовывоз звоним только если разрешено настройкой
|
||||||
|
AND (COALESCE(og.delivery_type, 'delivery') <> 'pickup' OR %s)
|
||||||
ORDER BY og.sms_sent_at ASC
|
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()]
|
return [dict(r) for r in cur.fetchall()]
|
||||||
|
|
||||||
def is_work_time(settings, now=None):
|
def is_work_time(settings, now=None):
|
||||||
|
|
@ -165,6 +168,19 @@ def get_exclusion_match(conn, phone):
|
||||||
""", (phone, phone))
|
""", (phone, phone))
|
||||||
return cur.fetchone()
|
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):
|
def build_call_file(group, settings, attempt_no, is_test_call=False):
|
||||||
"""Формирует содержимое call-файла."""
|
"""Формирует содержимое call-файла."""
|
||||||
phone = group.get("customer_phone_normalized") or group.get("customer_phone", "")
|
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 (завтра/послезавтра),
|
# Сценарий: в день добавления группы → A (завтра/послезавтра),
|
||||||
# со следующего дня → B (только завтра + половина дня).
|
# со следующего дня → B (только завтра + половина дня).
|
||||||
scenario = group.get("scenario") or resolve_scenario(group.get("created_at"))
|
scenario = group.get("scenario") or resolve_scenario(group.get("created_at"))
|
||||||
call_type = "dlv"
|
call_type = resolve_call_type(group)
|
||||||
|
|
||||||
lines = [
|
lines = [
|
||||||
f"Channel: PJSIP/{dial}@mmedia",
|
f"Channel: PJSIP/{dial}@mmedia",
|
||||||
|
|
@ -235,7 +251,8 @@ def get_queued_calls(conn):
|
||||||
"""Ручные запросы «Позвонить сейчас» из autocall_queue."""
|
"""Ручные запросы «Позвонить сейчас» из autocall_queue."""
|
||||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||||
cur.execute("""
|
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
|
FROM autocall_queue q
|
||||||
WHERE q.processed_at IS NULL
|
WHERE q.processed_at IS NULL
|
||||||
ORDER BY q.requested_at ASC
|
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"])
|
ids = tuple(r["order_group_id"] for r in rows if r["order_group_id"])
|
||||||
if ids:
|
if ids:
|
||||||
cur.execute("""
|
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
|
FROM order_groups og WHERE og.id IN %s
|
||||||
""", (ids,))
|
""", (ids,))
|
||||||
og = {r["id"]: r for r in cur.fetchall()}
|
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["customer_phone_normalized"] = g.get("customer_phone_normalized")
|
||||||
r.setdefault("customer_phone", g.get("customer_phone"))
|
r.setdefault("customer_phone", g.get("customer_phone"))
|
||||||
r.setdefault("created_at", g.get("created_at"))
|
r.setdefault("created_at", g.get("created_at"))
|
||||||
|
r.setdefault("delivery_type", g.get("delivery_type"))
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -327,7 +346,7 @@ def backfill_no_answer(conn, settings):
|
||||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT q.id, q.order_group_id, q.phone, q.is_test_call,
|
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
|
FROM autocall_queue q
|
||||||
WHERE q.processed_at IS NOT NULL
|
WHERE q.processed_at IS NOT NULL
|
||||||
AND q.processed_at < NOW() - make_interval(mins => 10)
|
AND q.processed_at < NOW() - make_interval(mins => 10)
|
||||||
|
|
@ -356,10 +375,11 @@ def backfill_no_answer(conn, settings):
|
||||||
# Телефон из заказа (или нормализованный из заявки для pure-теста)
|
# Телефон из заказа (или нормализованный из заявки для pure-теста)
|
||||||
phone = normalize_phone(q["phone"] or "")
|
phone = normalize_phone(q["phone"] or "")
|
||||||
group_created = None
|
group_created = None
|
||||||
|
group_dtype = None
|
||||||
if gid:
|
if gid:
|
||||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||||
cur.execute("""
|
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
|
FROM order_groups WHERE id = %s
|
||||||
""", (gid,))
|
""", (gid,))
|
||||||
grow = cur.fetchone() or {}
|
grow = cur.fetchone() or {}
|
||||||
|
|
@ -367,6 +387,7 @@ def backfill_no_answer(conn, settings):
|
||||||
or normalize_phone(grow.get("customer_phone") or "")
|
or normalize_phone(grow.get("customer_phone") or "")
|
||||||
or phone)
|
or phone)
|
||||||
group_created = grow.get("created_at")
|
group_created = grow.get("created_at")
|
||||||
|
group_dtype = grow.get("delivery_type")
|
||||||
# По исключённым номерам звонка не было — строку не пишем
|
# По исключённым номерам звонка не было — строку не пишем
|
||||||
# (статус «исключение» и так показывается из настроек).
|
# (статус «исключение» и так показывается из настроек).
|
||||||
if get_exclusion_match(conn, phone):
|
if get_exclusion_match(conn, phone):
|
||||||
|
|
@ -379,14 +400,15 @@ def backfill_no_answer(conn, settings):
|
||||||
else:
|
else:
|
||||||
attempt_no = 1
|
attempt_no = 1
|
||||||
test_mode = bool(q["is_test_call"] or settings.get("test_mode", True))
|
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:
|
with conn.cursor() as cur:
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
INSERT INTO autocall_log
|
INSERT INTO autocall_log
|
||||||
(order_group_id, customer_phone, attempt_no, scenario,
|
(order_group_id, customer_phone, attempt_no, scenario,
|
||||||
call_started_at, outcome, was_test_mode, channel_state)
|
call_started_at, outcome, was_test_mode, channel_state, call_type)
|
||||||
VALUES (%s, %s, %s, %s, %s, 'no_answer', %s, 'NoAnswer')
|
VALUES (%s, %s, %s, %s, %s, 'no_answer', %s, 'NoAnswer', %s)
|
||||||
""", (gid, phone, attempt_no, scenario,
|
""", (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()
|
conn.commit()
|
||||||
backfilled += 1
|
backfilled += 1
|
||||||
log.info(f"Backfilled no_answer for queue item {q['id']} (phone={phone})")
|
log.info(f"Backfilled no_answer for queue item {q['id']} (phone={phone})")
|
||||||
|
|
|
||||||
|
|
@ -285,6 +285,27 @@ export const AutocallChannelTab = () => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Самовывоз — отдельный выключатель: звонки клиентам, которые забирают со склада */}
|
||||||
|
<div className={`mb-4 rounded-xl border p-3 ${settings.pickup_calls_enabled ? "border-[var(--color-accent)] bg-[var(--color-accent-soft)]" : "border-[var(--color-border)] bg-[var(--color-surface)]"}`}>
|
||||||
|
<label className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-semibold text-[var(--color-text)]">🏪 Самовывоз</div>
|
||||||
|
<div className="mt-0.5 text-xs text-[var(--color-text-muted)]">
|
||||||
|
{settings.pickup_calls_enabled
|
||||||
|
? "Робот звонит и по самовывозу: предлагает забрать завтра или послезавтра"
|
||||||
|
: "Звонки по самовывозу выключены — звоним только по доставке"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => updateSetting("pickup_calls_enabled", !settings.pickup_calls_enabled)}
|
||||||
|
className={`relative h-7 w-12 shrink-0 rounded-full transition ${settings.pickup_calls_enabled ? "bg-[var(--color-accent)]" : "bg-[var(--color-border)]"}`}
|
||||||
|
>
|
||||||
|
<span className={`absolute top-0.5 h-6 w-6 rounded-full bg-white shadow transition ${settings.pickup_calls_enabled ? "left-[22px]" : "left-0.5"}`} />
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Интервалы и попытки */}
|
{/* Интервалы и попытки */}
|
||||||
<div className="mb-1 text-xs font-semibold text-[var(--color-text)]">📊 Интервалы и попытки</div>
|
<div className="mb-1 text-xs font-semibold text-[var(--color-text)]">📊 Интервалы и попытки</div>
|
||||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
|
@ -408,6 +429,7 @@ export const AutocallChannelTab = () => {
|
||||||
<div className="px-3 py-1.5 text-[var(--color-text)]">
|
<div className="px-3 py-1.5 text-[var(--color-text)]">
|
||||||
{fmtPhone(l.customer_phone)}
|
{fmtPhone(l.customer_phone)}
|
||||||
{l.was_test_mode && <div className="text-[10px] text-[var(--color-warning)]">🧪 тест</div>}
|
{l.was_test_mode && <div className="text-[10px] text-[var(--color-warning)]">🧪 тест</div>}
|
||||||
|
<div className="text-[10px] text-[var(--color-text-muted)]">{l.call_type === "pkp" ? "🏪 самовывоз" : "🚚 доставка"}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-3 py-1.5 text-[var(--color-text-muted)]">{l.attempt_no}</div>
|
<div className="px-3 py-1.5 text-[var(--color-text-muted)]">{l.attempt_no}</div>
|
||||||
<div className="px-3 py-1.5 text-[var(--color-text-muted)]">{l.duration_sec ? `${l.duration_sec}с` : "—"}</div>
|
<div className="px-3 py-1.5 text-[var(--color-text-muted)]">{l.duration_sec ? `${l.duration_sec}с` : "—"}</div>
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ const fmtTime = (ts) => {
|
||||||
|
|
||||||
export const TestCallPanel = () => {
|
export const TestCallPanel = () => {
|
||||||
const [phone, setPhone] = useState("");
|
const [phone, setPhone] = useState("");
|
||||||
|
const [callType, setCallType] = useState("dlv"); // dlv = доставка, pkp = самовывоз
|
||||||
const [status, setStatus] = useState("idle"); // idle | queueing | queued | dialing | done | error
|
const [status, setStatus] = useState("idle"); // idle | queueing | queued | dialing | done | error
|
||||||
const [result, setResult] = useState(null); // последняя строка autocall_log для этого теста
|
const [result, setResult] = useState(null); // последняя строка autocall_log для этого теста
|
||||||
const [startedAt, setStartedAt] = useState(null);
|
const [startedAt, setStartedAt] = useState(null);
|
||||||
|
|
@ -90,6 +91,7 @@ export const TestCallPanel = () => {
|
||||||
order_group_id: null,
|
order_group_id: null,
|
||||||
phone: "7" + (digits.length === 10 ? digits : digits.slice(digits.length - 10)),
|
phone: "7" + (digits.length === 10 ? digits : digits.slice(digits.length - 10)),
|
||||||
scenario: "A",
|
scenario: "A",
|
||||||
|
call_type: callType,
|
||||||
is_test_call: true,
|
is_test_call: true,
|
||||||
});
|
});
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
|
|
@ -152,6 +154,29 @@ export const TestCallPanel = () => {
|
||||||
Позвонит на введённый номер через АТС клиента и покажет, какой выбор сделал человек после голосового меню.
|
Позвонит на введённый номер через АТС клиента и покажет, какой выбор сделал человек после голосового меню.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{/* Тип сценария: доставка или самовывоз — от него зависит озвучка робота */}
|
||||||
|
<div className="mb-2 flex items-center gap-2">
|
||||||
|
<span className="text-[10px] font-medium text-[var(--color-text-muted)]">Сценарий:</span>
|
||||||
|
{[
|
||||||
|
{ key: "dlv", label: "🚚 Доставка" },
|
||||||
|
{ key: "pkp", label: "🏪 Самовывоз" },
|
||||||
|
].map((t) => (
|
||||||
|
<button
|
||||||
|
key={t.key}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setCallType(t.key)}
|
||||||
|
disabled={status === "queueing" || status === "queued" || status === "dialing"}
|
||||||
|
className={`rounded-full px-3 py-1 text-xs font-medium transition disabled:opacity-50 ${
|
||||||
|
callType === t.key
|
||||||
|
? "bg-[var(--color-accent)] text-white"
|
||||||
|
: "border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text-muted)] hover:bg-[var(--color-surface-strong)]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{t.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2 sm:flex-row">
|
<div className="flex flex-col gap-2 sm:flex-row">
|
||||||
<input
|
<input
|
||||||
type="tel"
|
type="tel"
|
||||||
|
|
@ -227,6 +252,7 @@ export const TestCallPanel = () => {
|
||||||
return (
|
return (
|
||||||
<div key={t.id} className="flex flex-wrap items-center gap-x-3 gap-y-0.5 rounded-lg bg-[var(--color-surface)] px-2 py-1.5 text-[11px]">
|
<div key={t.id} className="flex flex-wrap items-center gap-x-3 gap-y-0.5 rounded-lg bg-[var(--color-surface)] px-2 py-1.5 text-[11px]">
|
||||||
<span className="font-medium text-[var(--color-text)]">{fmtPhone(t.customer_phone)}</span>
|
<span className="font-medium text-[var(--color-text)]">{fmtPhone(t.customer_phone)}</span>
|
||||||
|
<span className="text-[10px] text-[var(--color-text-muted)]">{t.call_type === "pkp" ? "🏪 самовывоз" : "🚚 доставка"}</span>
|
||||||
<span className={`text-[var(--color-text-muted)]`}>{tm.icon} {tm.label}</span>
|
<span className={`text-[var(--color-text-muted)]`}>{tm.icon} {tm.label}</span>
|
||||||
{slot && <span className="text-[var(--color-text-muted)]">· {slot}</span>}
|
{slot && <span className="text-[var(--color-text-muted)]">· {slot}</span>}
|
||||||
<span className="text-[10px] text-[var(--color-text-muted)]">· {fmtTime(t.created_at)}</span>
|
<span className="text-[10px] text-[var(--color-text-muted)]">· {fmtTime(t.created_at)}</span>
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,11 @@ export const AutocallStatusCard = ({ order, userRole }) => {
|
||||||
const { error } = await supabase.from("autocall_queue").insert({
|
const { error } = await supabase.from("autocall_queue").insert({
|
||||||
order_group_id: order.id,
|
order_group_id: order.id,
|
||||||
phone: order.customer_phone || order.customerPhone || "",
|
phone: order.customer_phone || order.customerPhone || "",
|
||||||
|
// Тип озвучки: по самовывозу робот говорит «забрать», по доставке — «доставить».
|
||||||
|
// Если тип в объекте не пришёл, планировщик определит его по заказу сам.
|
||||||
|
...((order.delivery_type || order.deliveryType)
|
||||||
|
? { call_type: (order.delivery_type || order.deliveryType) === "pickup" ? "pkp" : "dlv" }
|
||||||
|
: {}),
|
||||||
});
|
});
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
setTimeout(() => setQueued(false), 3000);
|
setTimeout(() => setQueued(false), 3000);
|
||||||
|
|
@ -159,6 +164,7 @@ export const AutocallStatusCard = ({ order, userRole }) => {
|
||||||
<div className="flex flex-wrap items-center gap-2 text-[11px]">
|
<div className="flex flex-wrap items-center gap-2 text-[11px]">
|
||||||
<span className="text-[var(--color-text-muted)]">{fmtTime(c.created_at)}</span>
|
<span className="text-[var(--color-text-muted)]">{fmtTime(c.created_at)}</span>
|
||||||
<Badge tone={m.tone}>{m.icon} {m.label}</Badge>
|
<Badge tone={m.tone}>{m.icon} {m.label}</Badge>
|
||||||
|
{c.call_type === "pkp" && <span className="text-[10px] text-[var(--color-text-muted)]">🏪 самовывоз</span>}
|
||||||
{c.was_test_mode && <span className="text-[10px] text-[var(--color-warning)]">🧪 тест</span>}
|
{c.was_test_mode && <span className="text-[10px] text-[var(--color-warning)]">🧪 тест</span>}
|
||||||
</div>
|
</div>
|
||||||
{(slot || c.attempt_no > 1 || c.duration_sec > 0) && (
|
{(slot || c.attempt_no > 1 || c.duration_sec > 0) && (
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ type AutocallBody = {
|
||||||
customer_phone?: string;
|
customer_phone?: string;
|
||||||
attempt_no?: number;
|
attempt_no?: number;
|
||||||
scenario?: string;
|
scenario?: string;
|
||||||
|
call_type?: string;
|
||||||
call_started_at?: string;
|
call_started_at?: string;
|
||||||
answered_at?: string | null;
|
answered_at?: string | null;
|
||||||
duration_sec?: number | null;
|
duration_sec?: number | null;
|
||||||
|
|
@ -81,6 +82,7 @@ Deno.serve(async (request) => {
|
||||||
const campaignType = body.campaign_type || "autocall_slot";
|
const campaignType = body.campaign_type || "autocall_slot";
|
||||||
const attemptNo = body.attempt_no ?? 1;
|
const attemptNo = body.attempt_no ?? 1;
|
||||||
const scenario = body.scenario || "A";
|
const scenario = body.scenario || "A";
|
||||||
|
const callType = body.call_type === "pkp" ? "pkp" : "dlv";
|
||||||
const wasTest = body.was_test_mode ?? false;
|
const wasTest = body.was_test_mode ?? false;
|
||||||
|
|
||||||
// ── Идемпотентность: повторный curl с тем же uniqueid обновляет строку ──
|
// ── Идемпотентность: повторный curl с тем же uniqueid обновляет строку ──
|
||||||
|
|
@ -109,6 +111,7 @@ Deno.serve(async (request) => {
|
||||||
customer_phone: body.customer_phone,
|
customer_phone: body.customer_phone,
|
||||||
attempt_no: attemptNo,
|
attempt_no: attemptNo,
|
||||||
scenario,
|
scenario,
|
||||||
|
call_type: callType,
|
||||||
call_started_at: body.call_started_at || new Date().toISOString(),
|
call_started_at: body.call_started_at || new Date().toISOString(),
|
||||||
answered_at: body.answered_at || null,
|
answered_at: body.answered_at || null,
|
||||||
duration_sec: body.duration_sec ?? null,
|
duration_sec: body.duration_sec ?? null,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue